Category management - lexicon

This commit is contained in:
Thomas Andres Gomez 2023-11-15 14:16:26 +01:00
parent 020af02c29
commit fa7fcbeae6
24 changed files with 266 additions and 699 deletions

View file

@ -7,39 +7,14 @@ import androidx.compose.runtime.Stable
data class Lexicon( data class Lexicon(
val id: String, val id: String,
val name: String, val name: String,
val category: String?,
val diminutive: String?, val diminutive: String?,
val gender: Gender, val gender: String?,
val race: Race, val race: String?,
val status: String?, val status: String?,
val location: String?, val location: String?,
val portrait: List<Uri>, val portrait: List<Uri>,
val description: String?, val description: String?,
val history: String?, val history: String?,
val tags: 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,
}
}

View file

@ -6,7 +6,7 @@ import androidx.compose.runtime.Stable
@Stable @Stable
data class Quest( data class Quest(
val id: String, val id: String,
val group: String?, val category: String?,
val title: String, val title: String,
val entries: List<QuestEntry>, val entries: List<QuestEntry>,
) { ) {

View file

@ -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"
}
}

View file

@ -7,8 +7,6 @@ import javax.inject.Inject
class LexiconParser @Inject constructor( class LexiconParser @Inject constructor(
private val portraitParser: PortraitParser, private val portraitParser: PortraitParser,
private val genderParser: GenderParser,
private val raceParser: RaceParser,
) { ) {
@Throws(IncompatibleSheetStructure::class) @Throws(IncompatibleSheetStructure::class)
fun parse(sheet: ValueRange): List<Lexicon> = parserScope { fun parse(sheet: ValueRange): List<Lexicon> = parserScope {
@ -26,9 +24,10 @@ class LexiconParser @Inject constructor(
val lexicon = Lexicon( val lexicon = Lexicon(
id = "$name-${ids[name]}", id = "$name-${ids[name]}",
name = name, name = name,
category = row.parse(column = CATEGORY),
diminutive = row.parse(column = SHORT), diminutive = row.parse(column = SHORT),
gender = genderParser.parse(row.parse(column = GENDER)), gender = row.parse(column = GENDER),
race = raceParser.parser(row.parse(column = RACE)), race = row.parse(column = RACE),
status = row.parse(column = STATUS), status = row.parse(column = STATUS),
location = row.parse(column = LOCATION), location = row.parse(column = LOCATION),
portrait = portraitParser.parse(row.parse(column = PORTRAIT)), portrait = portraitParser.parse(row.parse(column = PORTRAIT)),
@ -47,6 +46,7 @@ class LexiconParser @Inject constructor(
companion object { companion object {
private val NAME = column("Nom") private val NAME = column("Nom")
private val CATEGORY = column("Catégorie")
private val SHORT = column("Diminutif") private val SHORT = column("Diminutif")
private val GENDER = column("Sexe") private val GENDER = column("Sexe")
private val RACE = column("Race") private val RACE = column("Race")
@ -60,6 +60,7 @@ class LexiconParser @Inject constructor(
private val COLUMNS private val COLUMNS
get() = listOf( get() = listOf(
NAME, NAME,
CATEGORY,
SHORT, SHORT,
GENDER, GENDER,
RACE, RACE,

View file

@ -22,7 +22,7 @@ class QuestParser @Inject constructor(
val entry = QuestEntry( val entry = QuestEntry(
sheetIndex = index, sheetIndex = index,
title = quest, title = quest,
group = item.parse(column = GROUP), group = item.parse(column = CATEGORY),
subtitle = item.parse(column = SUB_TITLE), subtitle = item.parse(column = SUB_TITLE),
complete = item.parseBool(column = COMPLETED) ?: false, complete = item.parseBool(column = COMPLETED) ?: false,
questGiver = item.parse(column = QUEST_GIVER), questGiver = item.parse(column = QUEST_GIVER),
@ -44,7 +44,7 @@ class QuestParser @Inject constructor(
Quest( Quest(
id = "$quest-1", // TODO refactor that when quest have ids in the google sheet. id = "$quest-1", // TODO refactor that when quest have ids in the google sheet.
title = quest, title = quest,
group = relatedEntries.firstNotNullOfOrNull { it.group }, category = relatedEntries.firstNotNullOfOrNull { it.group },
entries = relatedEntries, entries = relatedEntries,
) )
} }
@ -54,7 +54,7 @@ class QuestParser @Inject constructor(
companion object { companion object {
private val TITLE = column("Titre") 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 SUB_TITLE = column("Sous Titre")
private val COMPLETED = column("Compléter") private val COMPLETED = column("Compléter")
private val QUEST_GIVER = column("Commanditaire") private val QUEST_GIVER = column("Commanditaire")
@ -68,7 +68,7 @@ class QuestParser @Inject constructor(
private val COLUMNS private val COLUMNS
get() = listOf( get() = listOf(
TITLE, TITLE,
GROUP, CATEGORY,
SUB_TITLE, SUB_TITLE,
COMPLETED, COMPLETED,
QUEST_GIVER, QUEST_GIVER,

View file

@ -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"
}
}

View file

@ -21,7 +21,7 @@ class CategoryOrderRepository @Inject constructor(
var lastSuccessFullUpdate: Update = Update.INITIAL var lastSuccessFullUpdate: Update = Update.INITIAL
private set private set
fun finLexiconOrder(quest: String?): Int { fun findLexiconOrder(quest: String?): Int {
return _data.value[LEXICON]?.get(quest) ?: Int.MAX_VALUE return _data.value[LEXICON]?.get(quest) ?: Int.MAX_VALUE
} }

View file

@ -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)
}

View file

@ -8,7 +8,6 @@ import androidx.navigation.NavHostController
import androidx.navigation.NavOptionsBuilder import androidx.navigation.NavOptionsBuilder
import androidx.navigation.NavType import androidx.navigation.NavType
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.pixelized.rplexicon.data.model.Lexicon
import com.pixelized.rplexicon.ui.navigation.NavigationAnimation import com.pixelized.rplexicon.ui.navigation.NavigationAnimation
import com.pixelized.rplexicon.ui.navigation.animatedComposable import com.pixelized.rplexicon.ui.navigation.animatedComposable
import com.pixelized.rplexicon.ui.screens.lexicon.detail.LexiconDetailScreen 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 ROUTE = "LexiconDetail"
private const val ARG_ID = "id" private const val ARG_ID = "id"
private const val ARG_HIGHLIGHT = "highlight" 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 + val LEXICON_DETAIL_ROUTE = ROUTE +
"?${ARG_ID.ARG}" + "?${ARG_ID.ARG}" +
"&${ARG_HIGHLIGHT.ARG}" + "&${ARG_HIGHLIGHT.ARG}"
"&${ARG_RACE.ARG}" +
"&${ARG_HIGHLIGHT_RACE.ARG}" +
"&${ARG_GENDER.ARG}" +
"&${ARG_HIGHLIGHT_GENDER.ARG}"
@Stable @Stable
@Immutable @Immutable
data class LexiconDetailArgument( data class LexiconDetailArgument(
val id: String, val id: String,
val highlight: String?, val highlight: String?,
val race: Lexicon.Race,
val highlightRace: Boolean,
val gender: Lexicon.Gender,
val highlightGender: Boolean,
) )
val SavedStateHandle.lexiconDetailArgument: LexiconDetailArgument val SavedStateHandle.lexiconDetailArgument: LexiconDetailArgument
get() = LexiconDetailArgument( get() = LexiconDetailArgument(
id = get(ARG_ID) id = get(ARG_ID) ?: error("CharacterDetailArgument argument: $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"),
highlight = get(ARG_HIGHLIGHT), highlight = get(ARG_HIGHLIGHT),
) )
@ -67,18 +45,6 @@ fun NavGraphBuilder.composableLexiconDetail() {
type = NavType.StringType type = NavType.StringType
nullable = true 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, animation = NavigationAnimation.Push,
) { ) {
@ -89,17 +55,11 @@ fun NavGraphBuilder.composableLexiconDetail() {
fun NavHostController.navigateToLexiconDetail( fun NavHostController.navigateToLexiconDetail(
id: String, id: String,
highlight: String? = null, highlight: String? = null,
race: Lexicon.Race? = null,
gender: Lexicon.Gender? = null,
option: NavOptionsBuilder.() -> Unit = {}, option: NavOptionsBuilder.() -> Unit = {},
) { ) {
val route = ROUTE + val route = ROUTE +
"?$ARG_ID=$id" + "?$ARG_ID=$id" +
"&$ARG_HIGHLIGHT=$highlight" + "&$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}"
navigate(route = route, builder = option) navigate(route = route, builder = option)
} }

View file

@ -55,7 +55,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.R 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.AsyncImage
import com.pixelized.rplexicon.ui.composable.BackgroundImage import com.pixelized.rplexicon.ui.composable.BackgroundImage
import com.pixelized.rplexicon.ui.composable.FullScreenImageHandler 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.LocalScreenNavHost
import com.pixelized.rplexicon.ui.navigation.screens.navigateToCharacterSheet import com.pixelized.rplexicon.ui.navigation.screens.navigateToCharacterSheet
import com.pixelized.rplexicon.ui.theme.LexiconTheme 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.annotatedSpan
import com.pixelized.rplexicon.utilitary.extentions.annotatedString import com.pixelized.rplexicon.utilitary.extentions.annotatedString
import com.pixelized.rplexicon.utilitary.extentions.highlightRegex import com.pixelized.rplexicon.utilitary.extentions.highlightRegex
@ -75,16 +73,14 @@ import com.pixelized.rplexicon.utilitary.extentions.searchCriterion
data class LexiconDetailUio( data class LexiconDetailUio(
val name: String, val name: String,
val diminutive: String?, val diminutive: String?,
val gender: Lexicon.Gender, val gender: String?,
val race: Lexicon.Race, val race: String?,
val status: String?, val status: String?,
val location: String?, val location: String?,
val portrait: List<Uri>, val portrait: List<Uri>,
val description: String?, val description: String?,
val history: String?, val history: String?,
val search: String?, val search: String?,
val highlightGender: Boolean?,
val highlightRace: Boolean?,
val tags: String?, val tags: String?,
) )
@ -92,8 +88,8 @@ data class LexiconDetailUio(
data class AnnotatedLexiconDetailUio( data class AnnotatedLexiconDetailUio(
val name: AnnotatedString, val name: AnnotatedString,
val diminutive: AnnotatedString?, val diminutive: AnnotatedString?,
val gender: AnnotatedString, val gender: AnnotatedString?,
val race: AnnotatedString, val race: AnnotatedString?,
val portrait: List<Uri>, val portrait: List<Uri>,
val status: AnnotatedString?, val status: AnnotatedString?,
val location: AnnotatedString?, val location: AnnotatedString?,
@ -110,10 +106,8 @@ fun LexiconDetailUio.annotate(): AnnotatedLexiconDetailUio {
val highlight = remember { SpanStyle(color = colorScheme.primary) } val highlight = remember { SpanStyle(color = colorScheme.primary) }
val trimmedSearch = remember(search) { search.searchCriterion() } val trimmedSearch = remember(search) { search.searchCriterion() }
val highlightRegex = remember(search) { trimmedSearch.highlightRegex } 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( AnnotatedLexiconDetailUio(
portrait = portrait, portrait = portrait,
name = AnnotatedString( name = AnnotatedString(
@ -132,20 +126,12 @@ fun LexiconDetailUio.annotate(): AnnotatedLexiconDetailUio {
diminutive = diminutive?.let { diminutive = diminutive?.let {
highlightRegex.annotatedString(input = it, spanStyle = highlight) highlightRegex.annotatedString(input = it, spanStyle = highlight)
}, },
gender = AnnotatedString( gender = gender?.let { gender ->
text = gender, highlightRegex.annotatedString(input = gender, spanStyle = highlight)
spanStyles = when (highlightGender) { },
true -> listOf(AnnotatedString.Range(highlight, 0, gender.length)) race = race?.let { race ->
else -> emptyList() highlightRegex.annotatedString(input = race, spanStyle = highlight)
} },
),
race = AnnotatedString(
text = race,
spanStyles = when (highlightRace) {
true -> listOf(AnnotatedString.Range(highlight, 0, race.length))
else -> emptyList()
}
),
description = description?.let { description -> description = description?.let { description ->
highlightRegex.annotatedString(input = description, spanStyle = highlight) highlightRegex.annotatedString(input = description, spanStyle = highlight)
}, },
@ -457,8 +443,8 @@ private fun LexiconDetailPreview() {
LexiconDetailUio( LexiconDetailUio(
name = "Brulkhai", name = "Brulkhai",
diminutive = "./ Bru", diminutive = "./ Bru",
gender = Lexicon.Gender.FEMALE, gender = "Female",
race = Lexicon.Race.HALF_ORC, race = "Half-orc",
status = "Vivante", status = "Vivante",
location = "Manoir Durst", location = "Manoir Durst",
portrait = listOf( portrait = listOf(
@ -468,8 +454,6 @@ private fun LexiconDetailPreview() {
history = null, history = null,
tags = "protagoniste, brute", tags = "protagoniste, brute",
search = "Bru", search = "Bru",
highlightGender = true,
highlightRace = true,
) )
) )
} }

View file

@ -38,8 +38,6 @@ class LexiconDetailViewModel @Inject constructor(
history = source.history, history = source.history,
tags = source.tags, tags = source.tags,
search = argument.highlight, search = argument.highlight,
highlightGender = argument.highlightGender && argument.gender == source.gender,
highlightRace = argument.highlightRace && argument.race == source.race,
) )
} }

View file

@ -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,
)
}

View file

@ -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>,
)

View file

@ -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_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -15,7 +13,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight 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.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.LOS_FULL import com.pixelized.rplexicon.utilitary.LOS_FULL
import com.pixelized.rplexicon.utilitary.LOS_HOLLOW import com.pixelized.rplexicon.utilitary.LOS_HOLLOW
@ -37,8 +33,8 @@ data class LexiconItemUio(
val id: String, val id: String,
val name: String, val name: String,
val diminutive: String?, val diminutive: String?,
@StringRes val gender: Int, val gender: String?,
@StringRes val race: Int, val race: String?,
val isPlayingCharacter: Boolean = false, val isPlayingCharacter: Boolean = false,
val placeholder: Boolean = false, val placeholder: Boolean = false,
) { ) {
@ -48,8 +44,8 @@ data class LexiconItemUio(
id: String = "Brulkhai-1", id: String = "Brulkhai-1",
name: String = "Brulkhai", name: String = "Brulkhai",
diminutive: String? = null, diminutive: String? = null,
@StringRes gender: Int = R.string.gender_female_short, gender: String? = null,
@StringRes race: Int = R.string.race_half_orc, race: String? = null,
isPlayingCharacter: Boolean = false, isPlayingCharacter: Boolean = false,
placeholder: Boolean = false, placeholder: Boolean = false,
) = LexiconItemUio( ) = LexiconItemUio(
@ -122,27 +118,31 @@ fun LexiconItem(
) )
} }
Text( item.gender?.let {
modifier = Modifier Text(
.alignByBaseline() modifier = Modifier
.placeholder { item.placeholder }, .alignByBaseline()
style = typography.base.labelMedium, .placeholder { item.placeholder },
fontStyle = FontStyle.Italic, style = typography.base.labelMedium,
fontWeight = FontWeight.Light, fontStyle = FontStyle.Italic,
maxLines = 1, fontWeight = FontWeight.Light,
text = stringResource(id = item.gender) maxLines = 1,
) text = it,
)
}
Text( item.race?.let {
modifier = Modifier Text(
.alignByBaseline() modifier = Modifier
.placeholder { item.placeholder }, .alignByBaseline()
style = typography.base.labelMedium, .placeholder { item.placeholder },
fontStyle = FontStyle.Italic, style = typography.base.labelMedium,
fontWeight = FontWeight.Light, fontStyle = FontStyle.Italic,
maxLines = 1, fontWeight = FontWeight.Light,
text = stringResource(id = item.race) maxLines = 1,
) text = it,
)
}
} }
} }
} }

View file

@ -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.LocalScreenNavHost
import com.pixelized.rplexicon.ui.navigation.screens.navigateToLexiconDetail import com.pixelized.rplexicon.ui.navigation.screens.navigateToLexiconDetail
import com.pixelized.rplexicon.ui.navigation.screens.navigateToLexiconSearch 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.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.cell import com.pixelized.rplexicon.utilitary.extentions.cell
import com.pixelized.rplexicon.utilitary.extentions.lexicon import com.pixelized.rplexicon.utilitary.extentions.lexicon
@ -102,7 +103,7 @@ private fun LexiconScreenContent(
refreshState: PullRefreshState, refreshState: PullRefreshState,
refreshing: State<Boolean>, refreshing: State<Boolean>,
isFabExpended: State<Boolean>, isFabExpended: State<Boolean>,
items: State<List<LexiconItemUio>>, items: State<List<LexiconGroupUio>>,
onSearch: () -> Unit, onSearch: () -> Unit,
onItem: (LexiconItemUio) -> Unit, onItem: (LexiconItemUio) -> Unit,
) { ) {
@ -137,17 +138,31 @@ private fun LexiconScreenContent(
state = lazyColumnState, state = lazyColumnState,
contentPadding = MaterialTheme.lexicon.dimens.itemListPadding, contentPadding = MaterialTheme.lexicon.dimens.itemListPadding,
) { ) {
items( items.value.forEachIndexed { index, entry ->
items = items.value, entry.category?.let {
key = { it.id }, item(
contentType = { "Lexicon" }, contentType = { "Header" },
) { ) {
LexiconItem( LexiconCategory(
modifier = Modifier modifier = Modifier
.clickable { onItem(it) } .padding(top = if (index == 0) 0.dp else 16.dp)
.cell(), .padding(horizontal = 16.dp),
item = it, 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 { items = remember {
mutableStateOf( mutableStateOf(
listOf( listOf(
LexiconItemUio( LexiconGroupUio(
id = "Brulkhai-1", category = null,
name = "Brulkhai", items = listOf(
diminutive = "Bru", LexiconItemUio(
gender = R.string.gender_female_short, id = "Brulkhai-1",
race = R.string.race_half_orc, name = "Brulkhai",
) diminutive = "Bru",
gender = "Female",
race = "Half-orc",
)
),
),
) )
) )
}, },

View file

@ -1,36 +1,44 @@
package com.pixelized.rplexicon.ui.screens.lexicon.list package com.pixelized.rplexicon.ui.screens.lexicon.list
import android.app.Application
import android.util.Log import android.util.Log
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.pixelized.rplexicon.R 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.character.CharacterSheetRepository
import com.pixelized.rplexicon.data.repository.lexicon.CategoryOrderRepository
import com.pixelized.rplexicon.data.repository.lexicon.LexiconRepository import com.pixelized.rplexicon.data.repository.lexicon.LexiconRepository
import com.pixelized.rplexicon.ui.composable.error.FetchErrorUio 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.exceptions.IncompatibleSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.context
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class LexiconViewModel @Inject constructor( class LexiconViewModel @Inject constructor(
private val lexiconRepository: LexiconRepository, private val lexiconRepository: LexiconRepository,
private val characterSheetRepository: CharacterSheetRepository, private val characterSheetRepository: CharacterSheetRepository,
) : ViewModel() { private val orderRepository: CategoryOrderRepository,
application: Application,
) : AndroidViewModel(application) {
private val _isLoading = mutableStateOf(false) private val _isLoading = mutableStateOf(false)
val isLoading: State<Boolean> get() = _isLoading val isLoading: State<Boolean> get() = _isLoading
private val _items = mutableStateOf<List<LexiconItemUio>>(emptyList()) private val _items = mutableStateOf<List<LexiconGroupUio>>(emptyList())
val items: State<List<LexiconItemUio>> get() = _items val items: State<List<LexiconGroupUio>> get() = _items
private val _error = MutableSharedFlow<FetchErrorUio>() private val _error = MutableSharedFlow<FetchErrorUio>()
val error: SharedFlow<FetchErrorUio> get() = _error val error: SharedFlow<FetchErrorUio> get() = _error
@ -38,41 +46,36 @@ class LexiconViewModel @Inject constructor(
init { init {
viewModelScope.launch { viewModelScope.launch {
launch { launch {
lexiconRepository.data.collect { items -> orderRepository.data.combine(lexiconRepository.data) { _, lexicon -> lexicon }
_items.value = items .collect { items ->
.sortedBy { it.name } _items.value = items
.sortedBy { !characterSheetRepository.haveSheet(it.name) } .sortedBy { it.name }
.map { item -> .groupBy(
LexiconItemUio( keySelector = {
id = item.id, LexiconCategoryUio(
name = item.name, title = it.category
diminutive = item.diminutive?.takeIf { it.isNotBlank() } ?: context.getString(R.string.default_category_other)
?.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
}, },
race = when (item.race) { valueTransform = { item ->
Lexicon.Race.ELF -> R.string.race_elf LexiconItemUio(
Lexicon.Race.HALFLING -> R.string.race_halfling id = item.id,
Lexicon.Race.HUMAN -> R.string.race_human name = item.name,
Lexicon.Race.DWARF -> R.string.race_dwarf diminutive = item.diminutive?.let { "./ $it" },
Lexicon.Race.HALF_ELF -> R.string.race_half_elf gender = item.gender,
Lexicon.Race.HALF_ORC -> R.string.race_half_orc race = item.race,
Lexicon.Race.DRAGONBORN -> R.string.race_dragonborn isPlayingCharacter = characterSheetRepository.haveSheet(item.name)
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
}, },
isPlayingCharacter = characterSheetRepository.haveSheet(item.name)
) )
} .map { item ->
} LexiconGroupUio(
category = item.key,
items = item.value,
)
}
.sortedBy { orderRepository.findLexiconOrder(quest = it.category?.title) }
}
} }
launch { launch {
updateLexicon(force = false) updateLexicon(force = false)
@ -81,15 +84,19 @@ class LexiconViewModel @Inject constructor(
} }
suspend fun updateLexicon(force: Boolean) = coroutineScope { suspend fun updateLexicon(force: Boolean) = coroutineScope {
_isLoading.value = true withContext(Dispatchers.Main) {
val lexicon = async { _isLoading.value = true
fetchLexicon(force = force)
} }
val sheets = async { withContext(Dispatchers.IO) {
fetchCharacterSheet(force = force) 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) { 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 { companion object {
private const val TAG = "LexiconViewModel" private const val TAG = "LexiconViewModel"
} }

View file

@ -4,11 +4,8 @@ import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface 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.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.R import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.data.model.Lexicon
import com.pixelized.rplexicon.ui.theme.LexiconTheme 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.annotatedSpan
import com.pixelized.rplexicon.utilitary.extentions.annotatedString import com.pixelized.rplexicon.utilitary.extentions.annotatedString
import com.pixelized.rplexicon.utilitary.extentions.finderRegex import com.pixelized.rplexicon.utilitary.extentions.finderRegex
@ -46,31 +41,27 @@ class SearchItemUio(
val id: String, val id: String,
val name: String, val name: String,
val diminutive: String?, val diminutive: String?,
val gender: Lexicon.Gender, val gender: String?,
val race: Lexicon.Race, val race: String?,
val status: String?, val status: String?,
val location: String?, val location: String?,
val description: String?, val description: String?,
val history: String?, val history: String?,
val tags: String?, val tags: String?,
val search: String, val search: String,
val highlightGender: Boolean,
val highlightRace: Boolean,
) { ) {
companion object { companion object {
fun preview( fun preview(
id: String = "Brulkhai-1", id: String = "Brulkhai-1",
name: String = "Brulkhai", name: String = "Brulkhai",
diminutive: String? = "Bru", diminutive: String? = "Bru",
gender: Lexicon.Gender = Lexicon.Gender.FEMALE, gender: String? = "Female",
race: Lexicon.Race = Lexicon.Race.HALF_ORC, 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 lorsquelle est en bonne santé. Elle a les cheveux châtains, les yeux noisettes et la peau couleur gris-vert typique de son espèce. Dun 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 quelle considère plus faibles quelle.\n" + 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 lorsquelle est en bonne santé. Elle a les cheveux châtains, les yeux noisettes et la peau couleur gris-vert typique de son espèce. Dun 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 quelle considère plus faibles quelle.\n" +
"Dune 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 dun manque déducation et dune capacité limitée à gérer ses émotions quà une débilité congénitale.\n" + "Dune 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 dun manque déducation et dune capacité limitée à gérer ses émotions quà une débilité congénitale.\n" +
"Elle voue à la force un culte car cest par son expression quelle se sent vraiment vivante et éprouve de grandes difficultés vis à vis de ceux quelle nomme foshnu (bébé, chouineur en commun).", "Elle voue à la force un culte car cest par son expression quelle se sent vraiment vivante et éprouve de grandes difficultés vis à vis de ceux quelle nomme foshnu (bébé, chouineur en commun).",
history: String? = null, history: String? = null,
search: String = " ", search: String = "",
highlightGender: Boolean = false,
highlightRace: Boolean = false,
): SearchItemUio { ): SearchItemUio {
return SearchItemUio( return SearchItemUio(
id = id, id = id,
@ -84,8 +75,6 @@ class SearchItemUio(
history = history, history = history,
tags = null, tags = null,
search = search, search = search,
highlightGender = highlightGender,
highlightRace = highlightRace,
) )
} }
} }
@ -96,8 +85,8 @@ class AnnotatedSearchItemUio(
val id: String, val id: String,
val name: AnnotatedString, val name: AnnotatedString,
val diminutive: AnnotatedString?, val diminutive: AnnotatedString?,
val gender: AnnotatedString, val gender: AnnotatedString?,
val race: AnnotatedString, val race: AnnotatedString?,
val status: AnnotatedString?, val status: AnnotatedString?,
val location: AnnotatedString?, val location: AnnotatedString?,
val description: AnnotatedString?, val description: AnnotatedString?,
@ -113,10 +102,8 @@ private fun SearchItemUio.annotate(): AnnotatedSearchItemUio {
val trimmedSearch = remember(search) { search.searchCriterion() } val trimmedSearch = remember(search) { search.searchCriterion() }
val highlightRegex = remember(search) { trimmedSearch.highlightRegex } val highlightRegex = remember(search) { trimmedSearch.highlightRegex }
val finderRegex = remember(search) { trimmedSearch.finderRegex } 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( AnnotatedSearchItemUio(
id = id, id = id,
name = AnnotatedString( name = AnnotatedString(
@ -130,23 +117,11 @@ private fun SearchItemUio.annotate(): AnnotatedSearchItemUio {
input = diminutive ?: "", input = diminutive ?: "",
spanStyle = highlight spanStyle = highlight
), ),
gender = gender.let { gender = finderRegex?.foldAll(gender)?.let { gender ->
AnnotatedString( highlightRegex?.annotatedString(gender, spanStyle = highlight)
text = it,
spanStyles = when (highlightGender) {
true -> listOf(AnnotatedString.Range(highlight, 0, it.length))
else -> emptyList()
}
)
}, },
race = race.let { race = finderRegex?.foldAll(race)?.let { race ->
AnnotatedString( highlightRegex?.annotatedString(race, spanStyle = highlight)
text = it,
spanStyles = when (highlightRace) {
true -> listOf(AnnotatedString.Range(highlight, 0, it.length))
else -> emptyList()
}
)
}, },
status = finderRegex?.foldAll(status)?.let { status -> status = finderRegex?.foldAll(status)?.let { status ->
highlightRegex?.annotatedString(status, spanStyle = highlight) highlightRegex?.annotatedString(status, spanStyle = highlight)
@ -167,7 +142,6 @@ private fun SearchItemUio.annotate(): AnnotatedSearchItemUio {
} }
} }
@OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun SearchItem( fun SearchItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -186,10 +160,10 @@ fun SearchItem(
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 16.dp) .padding(vertical = 8.dp, horizontal = 16.dp)
.animateContentSize(), .animateContentSize(),
verticalArrangement = Arrangement.spacedBy(6.dp), verticalArrangement = Arrangement.spacedBy(space = 4.dp),
) { ) {
FlowRow( Row(
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) { ) {
Text( Text(
modifier = Modifier.alignByBaseline(), modifier = Modifier.alignByBaseline(),
@ -206,26 +180,24 @@ fun SearchItem(
text = it, text = it,
) )
} }
} annotatedItem.gender?.let {
Text(
Row( modifier = Modifier.alignByBaseline(),
modifier = Modifier.offset(y = (-2).dp), style = typography.labelMedium,
horizontalArrangement = Arrangement.spacedBy(4.dp), fontStyle = FontStyle.Italic,
) { maxLines = 1,
Text( text = it,
modifier = Modifier.alignByBaseline(), )
style = typography.labelMedium, }
fontStyle = FontStyle.Italic, annotatedItem.race?.let {
maxLines = 1, Text(
text = annotatedItem.gender modifier = Modifier.alignByBaseline(),
) style = typography.labelMedium,
Text( fontStyle = FontStyle.Italic,
modifier = Modifier.alignByBaseline(), maxLines = 1,
style = typography.labelMedium, text = it,
fontStyle = FontStyle.Italic, )
maxLines = 1, }
text = annotatedItem.race
)
} }
Column( Column(
@ -346,7 +318,7 @@ private class SearchItemPreviewProvider : PreviewParameterProvider<SearchItemUio
SearchItemUio.preview(search = "bru"), SearchItemUio.preview(search = "bru"),
SearchItemUio.preview(search = "Brulkhai"), SearchItemUio.preview(search = "Brulkhai"),
SearchItemUio.preview(search = "elle"), SearchItemUio.preview(search = "elle"),
SearchItemUio.preview(highlightGender = true), SearchItemUio.preview(search = "female"),
SearchItemUio.preview(highlightRace = true), SearchItemUio.preview(search = "orc"),
) )
} }

View file

@ -6,7 +6,6 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
@ -41,23 +40,17 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.NO_WINDOW_INSETS import com.pixelized.rplexicon.NO_WINDOW_INSETS
import com.pixelized.rplexicon.R 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.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.TextField
import com.pixelized.rplexicon.ui.composable.form.TextFieldUio import com.pixelized.rplexicon.ui.composable.form.TextFieldUio
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.navigation.screens.navigateToLexiconDetail import com.pixelized.rplexicon.ui.navigation.screens.navigateToLexiconDetail
import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.composable.stringResource
import com.pixelized.rplexicon.utilitary.extentions.lexicon import com.pixelized.rplexicon.utilitary.extentions.lexicon
@Stable @Stable
data class SearchFormUio( data class SearchFormUio(
val search: TextFieldUio, val search: TextFieldUio,
val gender: DropDownFieldUio<Lexicon.Gender>,
val race: DropDownFieldUio<Lexicon.Race>,
) )
@Composable @Composable
@ -81,8 +74,6 @@ fun SearchScreen(
screen.navigateToLexiconDetail( screen.navigateToLexiconDetail(
id = item.id, id = item.id,
highlight = form.search.value.value.takeIf { it.isNotEmpty() }, highlight = form.search.value.value.takeIf { it.isNotEmpty() },
race = form.race.value.value,
gender = form.gender.value.value,
) )
}, },
onBack = { onBack = {
@ -172,19 +163,6 @@ private fun SearchBox(
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
field = form.search, 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( Divider(
modifier = Modifier.padding(top = 16.dp), modifier = Modifier.padding(top = 16.dp),
color = MaterialTheme.lexicon.colorScheme.placeholder, color = MaterialTheme.lexicon.colorScheme.placeholder,
@ -203,16 +181,6 @@ private fun SearchScreenContentPreview() {
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
form = SearchFormUio( form = SearchFormUio(
search = TextFieldUio.preview(R.string.search_field_search), 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 { items = remember {
mutableStateOf( mutableStateOf(
@ -221,36 +189,36 @@ private fun SearchScreenContentPreview() {
id = "Brulkhai-1", id = "Brulkhai-1",
name = "Brulkhai", name = "Brulkhai",
diminutive = "Bru", diminutive = "Bru",
gender = Lexicon.Gender.FEMALE, gender = "Female",
race = Lexicon.Race.HALF_ORC, race = "Half-orc",
), ),
SearchItemUio.preview( SearchItemUio.preview(
id = "Léandre-1", id = "Léandre-1",
name = "Léandre", name = "Léandre",
diminutive = null, diminutive = null,
gender = Lexicon.Gender.MALE, gender = "Male",
race = Lexicon.Race.HUMAN, race = "Human",
), ),
SearchItemUio.preview( SearchItemUio.preview(
id = "Nélia-1", id = "Nélia-1",
name = "Nélia", name = "Nélia",
diminutive = null, diminutive = null,
gender = Lexicon.Gender.FEMALE, gender = "Female",
race = Lexicon.Race.ELF, race = "Elf",
), ),
SearchItemUio.preview( SearchItemUio.preview(
id = "Tigrane-1", id = "Tigrane-1",
name = "Tigrane", name = "Tigrane",
diminutive = null, diminutive = null,
gender = Lexicon.Gender.MALE, gender = "Male",
race = Lexicon.Race.TIEFLING, race = "Tiefling",
), ),
SearchItemUio.preview( SearchItemUio.preview(
id = "Unathana-1", id = "Unathana-1",
name = "Unathana", name = "Unathana",
diminutive = "Una", diminutive = "Una",
gender = Lexicon.Gender.FEMALE, gender = "Female",
race = Lexicon.Race.HALF_ELF, race = "Half-elf",
), ),
) )
) )

View file

@ -5,12 +5,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.pixelized.rplexicon.R import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.data.model.Lexicon 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.data.repository.lexicon.LexiconRepository
import com.pixelized.rplexicon.ui.composable.form.DropDownFieldUio
import com.pixelized.rplexicon.ui.composable.form.TextFieldUio import com.pixelized.rplexicon.ui.composable.form.TextFieldUio
import com.pixelized.rplexicon.utilitary.composable.stringResource
import com.pixelized.rplexicon.utilitary.extentions.searchCriterion import com.pixelized.rplexicon.utilitary.extentions.searchCriterion
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
@ -20,8 +16,6 @@ class SearchViewModel @Inject constructor(
repository: LexiconRepository, repository: LexiconRepository,
) : ViewModel() { ) : ViewModel() {
private val _search = mutableStateOf("") private val _search = mutableStateOf("")
private val _gender = mutableStateOf<Gender?>(null)
private val _race = mutableStateOf<Race?>(null)
val form = SearchFormUio( val form = SearchFormUio(
search = TextFieldUio( search = TextFieldUio(
@ -31,39 +25,25 @@ class SearchViewModel @Inject constructor(
_search.value = it _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 private var data: List<Lexicon> = repository.data.value
val filter = derivedStateOf { val filter = derivedStateOf {
data.filter { item -> 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 search = _search.value.searchCriterion().map { criteria ->
val name = item.name.contains(criteria, true) 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 diminutive = item.diminutive?.contains(criteria, true) == true
val status = item.status?.contains(criteria, true) == true val status = item.status?.contains(criteria, true) == true
val location = item.location?.contains(criteria, true) == true val location = item.location?.contains(criteria, true) == true
val description = item.description?.contains(criteria, true) == true val description = item.description?.contains(criteria, true) == true
val history = item.history?.contains(criteria, true) == true val history = item.history?.contains(criteria, true) == true
val tag = item.tags?.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 { }.map {
it.toSearchUio() it.toSearchUio()
}.sortedBy { }.sortedBy {
@ -73,8 +53,6 @@ class SearchViewModel @Inject constructor(
private fun Lexicon.toSearchUio( private fun Lexicon.toSearchUio(
search: String = _search.value, search: String = _search.value,
highlightGender: Boolean = this.gender == _gender.value,
highlightRace: Boolean = this.race == _race.value,
) = SearchItemUio( ) = SearchItemUio(
id = this.id, id = this.id,
name = this.name, name = this.name,
@ -86,8 +64,6 @@ class SearchViewModel @Inject constructor(
description = this.description, description = this.description,
history = this.history, history = this.history,
search = search, search = search,
highlightGender = highlightGender,
highlightRace = highlightRace,
tags = tags, tags = tags,
) )
} }

View file

@ -16,6 +16,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
@ -51,7 +52,7 @@ class QuestListViewModel @Inject constructor(
.groupBy( .groupBy(
keySelector = { keySelector = {
QuestCategoryUio( QuestCategoryUio(
title = it.group title = it.category
?: context.getString(R.string.default_category_other), ?: context.getString(R.string.default_category_other),
) )
}, },
@ -74,36 +75,27 @@ class QuestListViewModel @Inject constructor(
} }
} }
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
withContext(Dispatchers.Main) { update(force = false)
_isLoading.value = true
}
awaitAll(
async { updateQuests(force = false) },
async { updateCategoryOrder(force = false) },
)
withContext(Dispatchers.Main) {
_isLoading.value = false
}
} }
} }
} }
fun update(force: Boolean) { suspend fun update(force: Boolean) = coroutineScope {
viewModelScope.launch(Dispatchers.IO) { withContext(Dispatchers.Main) {
withContext(Dispatchers.Main) { _isLoading.value = true
_isLoading.value = true }
} withContext(Dispatchers.IO) {
awaitAll( awaitAll(
async { updateQuests(force = force) }, async { fetchQuests(force = force) },
async { updateCategoryOrder(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 { try {
if (force || repository.lastSuccessFullUpdate.shouldUpdate()) { if (force || repository.lastSuccessFullUpdate.shouldUpdate()) {
repository.fetchQuests() repository.fetchQuests()
@ -121,7 +113,7 @@ class QuestListViewModel @Inject constructor(
} }
} }
private suspend fun updateCategoryOrder(force: Boolean) { private suspend fun fetchCategoryOrder(force: Boolean) {
try { try {
if (force || order.lastSuccessFullUpdate.shouldUpdate()) { if (force || order.lastSuccessFullUpdate.shouldUpdate()) {
order.fetchCategoryOrder() order.fetchCategoryOrder()

View file

@ -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
}
}
)
}

View file

@ -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
}
)
}

View file

@ -25,28 +25,6 @@
<string name="generic_failure">Échec</string> <string name="generic_failure">Échec</string>
<string name="generic_failure_critical">ÉCHEC CRITIQUE</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_abjuration">Abjuration</string>
<string name="spell_school_divination">Divination</string> <string name="spell_school_divination">Divination</string>
<string name="spell_school_enchantment">Enchantement</string> <string name="spell_school_enchantment">Enchantement</string>

View file

@ -25,28 +25,6 @@
<string name="generic_failure">Failure</string> <string name="generic_failure">Failure</string>
<string name="generic_failure_critical">CRITICAL 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_abjuration">Abjuration</string>
<string name="spell_school_divination">Divination</string> <string name="spell_school_divination">Divination</string>
<string name="spell_school_enchantment">Enchantment</string> <string name="spell_school_enchantment">Enchantment</string>