diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..8e25798 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/pixelized/rplexicon/model/Lexicon.kt b/app/src/main/java/com/pixelized/rplexicon/model/Lexicon.kt index 55cf2fe..2cf6423 100644 --- a/app/src/main/java/com/pixelized/rplexicon/model/Lexicon.kt +++ b/app/src/main/java/com/pixelized/rplexicon/model/Lexicon.kt @@ -1,7 +1,9 @@ package com.pixelized.rplexicon.model import android.net.Uri +import androidx.compose.runtime.Stable +@Stable data class Lexicon( val id: Int, val name: String, @@ -12,12 +14,15 @@ data class Lexicon( val description: String?, val history: String?, ) { + + @Stable enum class Gender { MALE, FEMALE, - UNDETERMINED + UNDETERMINED, } + @Stable enum class Race { ELF, HALFLING, @@ -32,6 +37,6 @@ data class Lexicon( GENASI, DEEP_GNOME, GOLIATH, - UNDETERMINED + UNDETERMINED, } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/composable/ScrollingKeyboardHanlder.kt b/app/src/main/java/com/pixelized/rplexicon/ui/composable/ScrollingKeyboardHanlder.kt new file mode 100644 index 0000000..2b0c612 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/composable/ScrollingKeyboardHanlder.kt @@ -0,0 +1,18 @@ +package com.pixelized.rplexicon.ui.composable + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.platform.LocalSoftwareKeyboardController + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun ScrollingKeyboardHandler( + lazyListState: LazyListState +) { + val keyboard = LocalSoftwareKeyboardController.current + + if (lazyListState.isScrollInProgress) { + keyboard?.hide() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableCharacterDetail.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableCharacterDetail.kt index 840f5a6..d31e236 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableCharacterDetail.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableCharacterDetail.kt @@ -8,6 +8,7 @@ import androidx.navigation.NavHostController import androidx.navigation.NavOptionsBuilder import androidx.navigation.NavType import androidx.navigation.navArgument +import com.pixelized.rplexicon.model.Lexicon import com.pixelized.rplexicon.ui.navigation.NavigationAnimation import com.pixelized.rplexicon.ui.navigation.animatedComposable import com.pixelized.rplexicon.ui.screens.detail.CharacterDetailScreen @@ -15,17 +16,53 @@ import com.pixelized.rplexicon.utilitary.extentions.ARG private const val ROUTE = "CharacterDetail" private const val ARG_ID = "id" -val CHARACTER_DETAIL_ROUTE = "$ROUTE?${ARG_ID.ARG}" +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" + +//CharacterDetail +// ?id=0 +// &highlight=null +// &race=false +// &highlightRace=UNDETERMINED +// &gender=false +// &highlightGender=UNDETERMINED + + +val CHARACTER_DETAIL_ROUTE = ROUTE + + "?${ARG_ID.ARG}" + + "&${ARG_HIGHLIGHT.ARG}" + + "&${ARG_RACE.ARG}" + + "&${ARG_HIGHLIGHT_RACE.ARG}" + + "&${ARG_GENDER.ARG}" + + "&${ARG_HIGHLIGHT_GENDER.ARG}" @Stable @Immutable data class CharacterDetailArgument( val id: Int, + val highlight: String?, + val race: Lexicon.Race, + val highlightRace: Boolean, + val gender: Lexicon.Gender, + val highlightGender: Boolean, ) val SavedStateHandle.characterDetailArgument: CharacterDetailArgument get() = CharacterDetailArgument( - id = get(ARG_ID) ?: error("CharacterDetailArgument argument: id") + 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"), + highlight = get(ARG_HIGHLIGHT), ) fun NavGraphBuilder.composableCharacterDetail() { @@ -34,7 +71,23 @@ fun NavGraphBuilder.composableCharacterDetail() { arguments = listOf( navArgument(name = ARG_ID) { type = NavType.IntType - } + }, + navArgument(name = ARG_HIGHLIGHT) { + 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, ) { @@ -44,8 +97,18 @@ fun NavGraphBuilder.composableCharacterDetail() { fun NavHostController.navigateToCharacterDetail( id: Int, + highlight: String? = null, + race: Lexicon.Race? = null, + gender: Lexicon.Gender? = null, option: NavOptionsBuilder.() -> Unit = {}, ) { - val route = "$ROUTE?$ARG_ID=$id" + 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}" + navigate(route = route, builder = option) } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailScreen.kt index 8952d21..f9f7465 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailScreen.kt @@ -3,7 +3,6 @@ package com.pixelized.rplexicon.ui.screens.detail import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.net.Uri -import androidx.annotation.StringRes import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -51,14 +50,23 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.tooling.preview.Preview 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.model.Lexicon import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost 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 +import com.pixelized.rplexicon.utilitary.extentions.foldAll +import com.pixelized.rplexicon.utilitary.extentions.highlightRegex import com.pixelized.rplexicon.utilitary.rememberLoadingTransition import com.skydoves.landscapist.ImageOptions import com.skydoves.landscapist.glide.GlideImage @@ -66,15 +74,79 @@ import com.skydoves.landscapist.glide.GlideImageState @Stable data class CharacterDetailUio( - val name: String?, + val name: String, val diminutive: String?, - @StringRes val gender: Int, - @StringRes val race: Int, + val gender: Lexicon.Gender, + val race: Lexicon.Race, val portrait: List, val description: String?, val history: String?, + val search: String?, + val highlightGender: Boolean?, + val highlightRace: Boolean?, ) +@Stable +data class AnnotatedCharacterDetailUio( + val name: AnnotatedString, + val diminutive: AnnotatedString?, + val gender: AnnotatedString, + val race: AnnotatedString, + val portrait: List, + val description: AnnotatedString?, + val history: AnnotatedString?, +) + +@Composable +@Stable +fun CharacterDetailUio.annotated(): AnnotatedCharacterDetailUio { + val colorScheme = MaterialTheme.colorScheme + val highlight = remember { SpanStyle(color = colorScheme.primary) } + val highlightRegex = remember(search) { search.highlightRegex } + val finderRegex = remember(search) { search.finderRegex } + val gender = stringResource(id = gender, short = true) + val race = stringResource(id = race) + + return AnnotatedCharacterDetailUio( + name = AnnotatedString( + text = name, + spanStyles = highlightRegex?.annotatedSpan( + input = name, + spanStyle = highlight, + ) ?: emptyList() + ), + diminutive = highlightRegex?.annotatedString( + input = diminutive ?: "", + spanStyle = highlight + ), + gender = gender.let { + AnnotatedString( + text = it, + spanStyles = when (highlightGender) { + true -> listOf(AnnotatedString.Range(highlight, 0, it.length)) + else -> emptyList() + } + ) + }, + race = race.let { + AnnotatedString( + text = it, + spanStyles = when (highlightRace) { + true -> listOf(AnnotatedString.Range(highlight, 0, it.length)) + else -> emptyList() + } + ) + }, + description = finderRegex?.foldAll(description)?.let { description -> + highlightRegex?.annotatedString(description, spanStyle = highlight) + }, + history = finderRegex?.foldAll(history)?.let { history -> + highlightRegex?.annotatedString(history, spanStyle = highlight) + }, + portrait = portrait, + ) +} + @Composable fun CharacterDetailScreen( viewModel: CharacterDetailViewModel = hiltViewModel(), @@ -100,6 +172,7 @@ private fun CharacterDetailScreenContent( ) { val colorScheme = MaterialTheme.colorScheme val typography = MaterialTheme.typography + val annotatedItem = item.value.annotated() Scaffold( modifier = modifier, @@ -123,7 +196,7 @@ private fun CharacterDetailScreenContent( Box( modifier = Modifier.padding(paddingValues = paddingValues), ) { - item.value.portrait.firstOrNull()?.let { uri -> + annotatedItem.portrait.firstOrNull()?.let { uri -> Box( modifier = Modifier .fillMaxWidth() @@ -169,14 +242,14 @@ private fun CharacterDetailScreenContent( modifier = Modifier.padding(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(4.dp) ) { - item.value.name?.let { + annotatedItem.name.let { Text( modifier = Modifier.alignByBaseline(), style = typography.headlineSmall, text = it, ) } - item.value.diminutive?.let { + annotatedItem.diminutive?.let { Text( modifier = Modifier.alignByBaseline(), style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) }, @@ -190,14 +263,14 @@ private fun CharacterDetailScreenContent( ) { Text( style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) }, - text = stringResource(id = item.value.gender), + text = annotatedItem.gender, ) Text( style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) }, - text = stringResource(id = item.value.race), + text = annotatedItem.race, ) } - item.value.description?.let { + annotatedItem.description?.let { Text( modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp), style = typography.titleMedium, @@ -216,7 +289,7 @@ private fun CharacterDetailScreenContent( text = it, ) } - item.value.history?.let { + annotatedItem.history?.let { Text( modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp), style = typography.titleMedium, @@ -228,7 +301,7 @@ private fun CharacterDetailScreenContent( text = it, ) } - if (item.value.portrait.isNotEmpty()) { + if (annotatedItem.portrait.isNotEmpty()) { val configuration = LocalConfiguration.current val maxSize = remember { (configuration.screenWidthDp.dp - 16.dp * 2) } Text( @@ -240,7 +313,7 @@ private fun CharacterDetailScreenContent( contentPadding = PaddingValues(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - items(items = item.value.portrait) { + items(items = annotatedItem.portrait) { val transition = rememberLoadingTransition { it } GlideImage( modifier = Modifier @@ -298,13 +371,16 @@ private fun CharacterDetailScreenContentPreview() { CharacterDetailUio( name = "Brulkhai", diminutive = "./ Bru", - gender = R.string.gender_female, - race = R.string.race_half_orc, + gender = Lexicon.Gender.FEMALE, + race = Lexicon.Race.HALF_ORC, portrait = listOf( Uri.parse("https://cdnb.artstation.com/p/assets/images/images/003/024/889/large/bayard-wu-0716.jpg?1468642855"), ), description = "Brulkhai, ou plus simplement Bru, est 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. 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. 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 = null, + search = "Bru", + highlightGender = true, + highlightRace = true, ) ) } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailViewModel.kt index 0547e45..9d017b7 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailViewModel.kt @@ -4,8 +4,6 @@ import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel -import com.pixelized.rplexicon.R -import com.pixelized.rplexicon.model.Lexicon import com.pixelized.rplexicon.repository.LexiconRepository import com.pixelized.rplexicon.ui.navigation.screens.characterDetailArgument import dagger.hilt.android.lifecycle.HiltViewModel @@ -13,40 +11,28 @@ import javax.inject.Inject @HiltViewModel class CharacterDetailViewModel @Inject constructor( - private val savedStateHandle: SavedStateHandle, - private val repository: LexiconRepository, + savedStateHandle: SavedStateHandle, + repository: LexiconRepository, ) : ViewModel() { - val character: State = mutableStateOf(init()) + val character: State - private fun init(): CharacterDetailUio { - val source = repository.data.value[savedStateHandle.characterDetailArgument.id] - return CharacterDetailUio( - name = source.name, - diminutive = source.diminutive?.let { "./ $it" }, - gender = when (source.gender) { - Lexicon.Gender.MALE -> R.string.gender_male - Lexicon.Gender.FEMALE -> R.string.gender_female - Lexicon.Gender.UNDETERMINED -> R.string.gender_undetermined - }, - race = when (source.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 - }, - portrait = source.portrait, - description = source.description, - history = source.history, + init { + val argument = savedStateHandle.characterDetailArgument + val source = repository.data.value[argument.id] + + character = mutableStateOf( + CharacterDetailUio( + name = source.name, + diminutive = source.diminutive?.let { "./ $it" }, + gender = source.gender, + race = source.race, + portrait = source.portrait, + description = source.description, + history = source.history, + search = argument.highlight, + highlightGender = argument.highlightGender && argument.gender == source.gender, + highlightRace = argument.highlightRace && argument.race == source.race, + ) ) } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconItem.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconItem.kt index 03f62d2..0af7cb3 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconItem.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconItem.kt @@ -4,6 +4,7 @@ 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.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow @@ -16,6 +17,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontStyle @@ -55,7 +57,10 @@ fun LexiconItem( ) { val typography = MaterialTheme.typography - Surface(modifier = modifier) { + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { Column( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconScreen.kt index 135fccd..947af39 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconScreen.kt @@ -12,13 +12,13 @@ import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.with -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.lazy.LazyColumn @@ -34,6 +34,7 @@ import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -66,6 +67,7 @@ import com.pixelized.rplexicon.ui.screens.lexicon.LexiconErrorUio.Default import com.pixelized.rplexicon.ui.screens.lexicon.LexiconErrorUio.Permission import com.pixelized.rplexicon.ui.screens.lexicon.LexiconErrorUio.Structure import com.pixelized.rplexicon.ui.theme.LexiconTheme +import com.pixelized.rplexicon.utilitary.extentions.lexicon import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch @@ -131,8 +133,9 @@ fun LexiconScreen( } @OptIn( - ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class, - ExperimentalFoundationApi::class, ExperimentalAnimationApi::class + ExperimentalMaterial3Api::class, + ExperimentalMaterialApi::class, + ExperimentalAnimationApi::class, ) @Composable private fun LexiconScreenContent( @@ -208,7 +211,7 @@ private fun LexiconScreenContent( ) { items(count = 6) { LexiconItem( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.heightIn(min = MaterialTheme.lexicon.dimens.item), item = LexiconItemUio.Brulkhai, ) } @@ -231,8 +234,8 @@ private fun LexiconScreenContent( ) { LexiconItem( modifier = Modifier - .animateItemPlacement() - .clickable { onItem(it) }, + .clickable { onItem(it) } + .heightIn(min = MaterialTheme.lexicon.dimens.item), item = it, ) } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchItem.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchItem.kt new file mode 100644 index 0000000..b976bb7 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchItem.kt @@ -0,0 +1,274 @@ +package com.pixelized.rplexicon.ui.screens.search + +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 +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +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.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 +import com.pixelized.rplexicon.utilitary.extentions.foldAll +import com.pixelized.rplexicon.utilitary.extentions.highlightRegex + +@Stable +class SearchItemUio( + val id: Int, + val name: String, + val diminutive: String?, + val gender: Lexicon.Gender, + val race: Lexicon.Race, + val description: String?, + val history: String?, + val search: String, + val highlightGender: Boolean, + val highlightRace: Boolean, +) { + companion object { + fun preview( + id: Int = 0, + name: String = "Brulkhai", + diminutive: String? = "Bru", + gender: Lexicon.Gender = Lexicon.Gender.FEMALE, + race: Lexicon.Race = Lexicon.Race.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, + ): SearchItemUio { + return SearchItemUio( + id = id, + name = name, + diminutive = diminutive, + gender = gender, + race = race, + description = description, + history = history, + search = search, + highlightGender = highlightGender, + highlightRace = highlightRace, + ) + } + } +} + +@Stable +class AnnotatedSearchItemUio( + val id: Int, + val name: AnnotatedString, + val diminutive: AnnotatedString?, + val gender: AnnotatedString, + val race: AnnotatedString, + val description: AnnotatedString?, + val history: AnnotatedString?, +) + +@Composable +@Stable +private fun SearchItemUio.annotate(): AnnotatedSearchItemUio { + val colorScheme = MaterialTheme.colorScheme + val highlight = remember { SpanStyle(color = colorScheme.primary) } + val highlightRegex = remember(search) { search.highlightRegex } + val finderRegex = remember(search) { search.finderRegex } + val gender = stringResource(id = gender, short = true) + val race = stringResource(id = race) + + return remember(search) { + AnnotatedSearchItemUio( + id = id, + name = AnnotatedString( + text = name, + spanStyles = highlightRegex?.annotatedSpan( + input = name, + spanStyle = highlight, + ) ?: emptyList() + ), + diminutive = highlightRegex?.annotatedString( + input = diminutive ?: "", + spanStyle = highlight + ), + gender = gender.let { + AnnotatedString( + text = it, + spanStyles = when (highlightGender) { + true -> listOf(AnnotatedString.Range(highlight, 0, it.length)) + else -> emptyList() + } + ) + }, + race = race.let { + AnnotatedString( + text = it, + spanStyles = when (highlightRace) { + true -> listOf(AnnotatedString.Range(highlight, 0, it.length)) + else -> emptyList() + } + ) + }, + description = finderRegex?.foldAll(description)?.let { description -> + highlightRegex?.annotatedString(description, spanStyle = highlight) + }, + history = finderRegex?.foldAll(history)?.let { history -> + highlightRegex?.annotatedString(history, spanStyle = highlight) + }, + ) + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun SearchItem( + modifier: Modifier = Modifier, + item: SearchItemUio, +) { + val typography = MaterialTheme.typography + val colorScheme = MaterialTheme.colorScheme + val annotatedItem = item.annotate() + + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp, horizontal = 16.dp) + .animateContentSize(), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + modifier = Modifier.alignByBaseline(), + style = remember { typography.bodyLarge.copy(fontWeight = FontWeight.Bold) }, + maxLines = 1, + text = annotatedItem.name, + ) + annotatedItem.diminutive?.let { + Text( + modifier = Modifier.alignByBaseline(), + style = typography.labelMedium, + maxLines = 1, + text = it, + ) + } + } + + Row( + modifier = Modifier.offset(y = (-2).dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + modifier = Modifier.alignByBaseline(), + style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) }, + maxLines = 1, + text = annotatedItem.gender + ) + Text( + modifier = Modifier.alignByBaseline(), + style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) }, + maxLines = 1, + text = annotatedItem.race + ) + } + + Column( + modifier = Modifier.drawBehind { + drawLine( + color = colorScheme.primary, + start = Offset(0f, 0f), + end = Offset(0f, size.height), + strokeWidth = 2.dp.toPx() + ) + }, + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + annotatedItem.description?.let { + Column( + modifier = Modifier.padding(start = 8.dp) + ) { + Text( + style = remember { typography.labelSmall.copy(fontWeight = FontWeight.Bold) }, + text = stringResource(id = R.string.search_item_description), + ) + Text( + style = typography.labelSmall, + text = it, + ) + } + } + annotatedItem.history?.let { + Column( + modifier = Modifier.padding(start = 8.dp), + ) { + Text( + style = remember { typography.labelSmall.copy(fontWeight = FontWeight.Bold) }, + text = stringResource(id = R.string.search_item_history), + ) + Text( + style = typography.labelSmall, + text = it, + ) + } + } + } + } + } +} + +@Composable +@Preview +private fun SearchItemPreview( + @PreviewParameter(SearchItemPreviewProvider::class) preview: SearchItemUio, +) { + LexiconTheme { + Surface { + SearchItem( + item = preview, + ) + } + } +} + +private class SearchItemPreviewProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + SearchItemUio.preview(), + SearchItemUio.preview(search = "bru"), + SearchItemUio.preview(search = "Brulkhai"), + SearchItemUio.preview(search = "elle"), + SearchItemUio.preview(highlightGender = true), + SearchItemUio.preview(highlightRace = true), + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchScreen.kt index 4efdcb9..b0f21a7 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchScreen.kt @@ -10,6 +10,7 @@ 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 import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding @@ -21,6 +22,7 @@ import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -30,6 +32,7 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext @@ -41,15 +44,15 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.pixelized.rplexicon.NO_WINDOW_INSETS import com.pixelized.rplexicon.R import com.pixelized.rplexicon.model.Lexicon +import com.pixelized.rplexicon.ui.composable.ScrollingKeyboardHandler 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.navigateToCharacterDetail -import com.pixelized.rplexicon.ui.screens.lexicon.LexiconItem -import com.pixelized.rplexicon.ui.screens.lexicon.LexiconItemUio import com.pixelized.rplexicon.ui.theme.LexiconTheme +import com.pixelized.rplexicon.utilitary.extentions.lexicon @Stable data class SearchFormUio( @@ -63,6 +66,7 @@ fun SearchScreen( viewModel: SearchViewModel = hiltViewModel(), ) { val screen = LocalScreenNavHost.current + val lazyState = rememberLazyListState() Surface { SearchScreenContent( @@ -70,15 +74,25 @@ fun SearchScreen( .fillMaxSize() .systemBarsPadding() .imePadding(), + lazyColumnState = lazyState, items = viewModel.filter, form = viewModel.form, - onItem = { - screen.navigateToCharacterDetail(id = it.id) + onItem = { item -> + val form = viewModel.form + screen.navigateToCharacterDetail( + id = item.id, + highlight = form.search.value.value.takeIf { it.isNotEmpty() }, + race = form.race.value.value.first, + gender = form.gender.value.value.first, + ) }, onBack = { screen.popBackStack() } ) + ScrollingKeyboardHandler( + lazyListState = lazyState, + ) } } @@ -87,10 +101,10 @@ fun SearchScreen( private fun SearchScreenContent( modifier: Modifier = Modifier, lazyColumnState: LazyListState = rememberLazyListState(), - items: State>, + items: State>, form: SearchFormUio, onBack: () -> Unit, - onItem: (LexiconItemUio) -> Unit, + onItem: (SearchItemUio) -> Unit, ) { Scaffold( modifier = modifier, @@ -147,12 +161,12 @@ private fun SearchScreenContent( items( items = items.value, key = { it.id }, - contentType = { "Lexicon" }, + contentType = { "Search" }, ) { - LexiconItem( + SearchItem( modifier = Modifier - .animateItemPlacement() - .clickable { onItem(it) }, + .clickable { onItem(it) } + .heightIn(min = MaterialTheme.lexicon.dimens.item), item = it, ) } @@ -185,12 +199,17 @@ private fun SearchScreenContentPreview() { items = remember { mutableStateOf( listOf( - LexiconItemUio( + SearchItemUio( id = 0, name = "Brulkhai", diminutive = "Bru", - gender = R.string.gender_female_short, - race = R.string.race_half_orc, + gender = Lexicon.Gender.FEMALE, + race = Lexicon.Race.HALF_ORC, + description = null, + history = null, + search = "", + highlightGender = false, + highlightRace = false, ) ) ) diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchViewModel.kt index 78f44dd..0b4be7c 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchViewModel.kt @@ -10,86 +10,77 @@ import com.pixelized.rplexicon.model.Lexicon.Race import com.pixelized.rplexicon.repository.LexiconRepository import com.pixelized.rplexicon.ui.composable.form.DropDownFieldUio import com.pixelized.rplexicon.ui.composable.form.TextFieldUio -import com.pixelized.rplexicon.ui.screens.lexicon.LexiconItemUio import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel class SearchViewModel @Inject constructor( - private val repository: LexiconRepository, + repository: LexiconRepository, ) : ViewModel() { - private val search = mutableStateOf("") - private val gender = mutableStateOf>(null to "") - private val race = mutableStateOf>(null to "") + private val _search = mutableStateOf("") + private val _gender = mutableStateOf>(null to "") + private val _race = mutableStateOf>(null to "") val form = SearchFormUio( search = TextFieldUio( label = R.string.search_field_search, - value = search, + value = _search, onValueChange = { - search.value = it + _search.value = it } ), gender = DropDownFieldUio( label = R.string.search_field_gender, values = genders(), - value = gender, - onValueChange = { id, value -> gender.value = id to value }, + value = _gender, + onValueChange = { id, value -> _gender.value = id to value }, ), race = DropDownFieldUio( label = R.string.search_field_race, values = races(), - value = race, - onValueChange = { id, value -> race.value = id to value }, + value = _race, + onValueChange = { id, value -> _race.value = id to value }, ), ) private var data: List = repository.data.value val filter = derivedStateOf { - data - .filter { item -> - val gender = gender.value.first?.let { it == item.gender } - val race = race.value.first?.let { it == item.race } - val search = search.value.takeIf { it.isNotEmpty() }?.let { - val name = item.name.contains(search.value, true) - val diminutive = item.diminutive?.contains(search.value, true) == true - val description = item.description?.contains(search.value, true) == true - val history = item.history?.contains(search.value, true) == true - name || diminutive || description || history - } - (gender == null || gender) && (race == null || race) && (search == null || search) - }.map { - LexiconItemUio( - id = it.id, - name = it.name, - diminutive = it.diminutive?.takeIf { it.isNotBlank() }?.let { "./ $it" }, - gender = when (it.gender) { - Gender.MALE -> R.string.gender_male_short - Gender.FEMALE -> R.string.gender_female_short - Gender.UNDETERMINED -> R.string.gender_undetermined_short - }, - race = when (it.race) { - Race.ELF -> R.string.race_elf - Race.HALFLING -> R.string.race_halfling - Race.HUMAN -> R.string.race_human - Race.DWARF -> R.string.race_dwarf - Race.HALF_ELF -> R.string.race_half_elf - Race.HALF_ORC -> R.string.race_half_orc - Race.DRAGONBORN -> R.string.race_dragonborn - Race.GNOME -> R.string.race_gnome - Race.TIEFLING -> R.string.race_tiefling - Race.AARAKOCRA -> R.string.race_aarakocra - Race.GENASI -> R.string.race_genasi - Race.DEEP_GNOME -> R.string.race_deep_gnome - Race.GOLIATH -> R.string.race_goliath - Race.UNDETERMINED -> R.string.race_undetermined - }, - ) + data.filter { item -> + val gender = _gender.value.first?.let { it == item.gender } + val race = _race.value.first?.let { it == item.race } + val search = _search.value.takeIf { it.isNotEmpty() }?.let { + val name = item.name.contains(_search.value, true) + val diminutive = item.diminutive?.contains(_search.value, true) == true + val description = item.description?.contains(_search.value, true) == true + val history = item.history?.contains(_search.value, true) == true + name || diminutive || description || history } - .sortedBy { it.name } + (gender == null || gender) && (race == null || race) && (search == null || search) + }.map { + it.toSearchUio() + }.sortedBy { + it.name + } } + private fun Lexicon.toSearchUio( + search: String = _search.value, + highlightGender: Boolean = this.gender == _gender.value.first, + highlightRace: Boolean = this.race == _race.value.first, + ) = SearchItemUio( + id = this.id, + name = this.name, + diminutive = diminutive?.takeIf { it.isNotBlank() }?.let { "./ $it" }, + gender = this.gender, + race = this.race, + description = this.description, + history = this.history, + search = search, + highlightGender = highlightGender, + highlightRace = highlightRace, + ) + companion object { private fun genders() = listOf( Gender.MALE to R.string.gender_male, diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/theme/Theme.kt b/app/src/main/java/com/pixelized/rplexicon/ui/theme/Theme.kt index 99cec13..6d6c808 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/theme/Theme.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/theme/Theme.kt @@ -15,6 +15,8 @@ import androidx.core.view.WindowCompat import com.pixelized.rplexicon.ui.theme.colors.LexiconColors import com.pixelized.rplexicon.ui.theme.colors.darkColorScheme import com.pixelized.rplexicon.ui.theme.colors.lightColorScheme +import com.pixelized.rplexicon.ui.theme.dimen.LexiconDimens +import com.pixelized.rplexicon.ui.theme.dimen.lexiconDimen import com.pixelized.rplexicon.ui.theme.shape.LexiconShapes import com.pixelized.rplexicon.ui.theme.shape.lexiconShapes @@ -26,6 +28,7 @@ val LocalLexiconTheme = compositionLocalOf { data class LexiconTheme( val colorScheme: LexiconColors, val shapes: LexiconShapes, + val dimens: LexiconDimens, ) @Composable @@ -39,7 +42,8 @@ fun LexiconTheme( true -> darkColorScheme() else -> lightColorScheme() }, - shapes = lexiconShapes() + shapes = lexiconShapes(), + dimens = lexiconDimen(), ) } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/theme/dimen/LexiconDimens.kt b/app/src/main/java/com/pixelized/rplexicon/ui/theme/dimen/LexiconDimens.kt new file mode 100644 index 0000000..6b0aad6 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/theme/dimen/LexiconDimens.kt @@ -0,0 +1,18 @@ +package com.pixelized.rplexicon.ui.theme.dimen + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Stable +@Immutable +data class LexiconDimens( + val item: Dp +) + +fun lexiconDimen( + item: Dp = 48.dp +) = LexiconDimens( + item = item, +) \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/composable/StringResource+Gender.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/composable/StringResource+Gender.kt new file mode 100644 index 0000000..fae62d4 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/composable/StringResource+Gender.kt @@ -0,0 +1,27 @@ +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.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 + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/composable/StringResource+Race.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/composable/StringResource+Race.kt new file mode 100644 index 0000000..3f90f94 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/composable/StringResource+Race.kt @@ -0,0 +1,30 @@ +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.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 + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/RegexEx.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/RegexEx.kt new file mode 100644 index 0000000..6d52988 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/RegexEx.kt @@ -0,0 +1,46 @@ +package com.pixelized.rplexicon.utilitary.extentions + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle + +fun Regex?.foldAll( + input: CharSequence?, + startIndex: Int = 0, +): String? = input?.let { + var previous: MatchResult? = null + this?.findAll(it, startIndex)?.fold("") { acc, item -> + val dummy = acc + when { + previous == null && item.range.first == 0 -> item.value + previous == null && item.range.first != 0 -> "...${item.value}" + item.range.first <= (previous?.range?.last ?: 0) + 1 -> item.value + else -> " ... ${item.value}" + } + previous = item + dummy + } +}?.takeIf { it.isNotEmpty() }?.let { "$it..." } + +fun Regex.annotatedSpan( + input: String, + startIndex: Int = 0, + spanStyle: SpanStyle +): List> { + return findAll(input = input, startIndex = startIndex).map { + AnnotatedString.Range( + item = spanStyle, + start = it.range.first, + end = it.range.last + 1 + ) + }.toList() +} + +fun Regex.annotatedString( + input: String, + startIndex: Int = 0, + spanStyle: SpanStyle +): AnnotatedString { + return AnnotatedString( + text = input, + spanStyles = annotatedSpan(input, startIndex, spanStyle) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/StringEt.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/StringEt.kt deleted file mode 100644 index 7f05ee4..0000000 --- a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/StringEt.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.pixelized.rplexicon.utilitary.extentions - -val String.ARG: String get() = "$this={$this}" \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/StringEx.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/StringEx.kt new file mode 100644 index 0000000..496996d --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/StringEx.kt @@ -0,0 +1,17 @@ +package com.pixelized.rplexicon.utilitary.extentions + +val String.ARG: String get() = "$this={$this}" + +val String?.highlightRegex: Regex? + get() = this?.takeIf { it.isNotEmpty() }?.let { + Regex(pattern = Regex.escape(it), option = RegexOption.IGNORE_CASE) + } + +val String?.finderRegex: Regex? + get() = this?.takeIf { it.isNotEmpty() }?.let { + Regex( + pattern = "\\w*.{0,30}${Regex.escape(it)}\\w*.{0,30}\\w*", + option = RegexOption.IGNORE_CASE, + ) + } + diff --git a/app/src/main/res/drawable/ic_dungeons_and_dragons_48dp.xml b/app/src/main/res/drawable/ic_dungeons_and_dragons_48dp.xml new file mode 100644 index 0000000..4339564 --- /dev/null +++ b/app/src/main/res/drawable/ic_dungeons_and_dragons_48dp.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 2b068d1..2172512 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,30 +1,23 @@ - - - - - - - - - - \ No newline at end of file + android:viewportWidth="48" + android:viewportHeight="48"> + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 56% rename from app/src/main/res/mipmap-anydpi/ic_launcher.xml rename to app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 6f3b755..7353dbd 100644 --- a/app/src/main/res/mipmap-anydpi/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,5 @@ - - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 56% rename from app/src/main/res/mipmap-anydpi/ic_launcher_round.xml rename to app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 6f3b755..7353dbd 100644 --- a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,6 +1,5 @@ - - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp index c209e78..6c29f96 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index b2dfe3d..a5eb983 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp index 4f0f1d6..d6b8a88 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index 62b611d..aacfbe3 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index 948a307..75ae9d9 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index 1b9a695..c9df960 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index 28d4b77..03f0924 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index 9287f50..d91f753 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index aa7d642..12bfe60 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 9126ae3..592575f 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 8d94de1..8ff55a4 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -39,4 +39,6 @@ Rechercher Race Sexe + Description : + Histoire : \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..0530591 --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #1C1B1F + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 745a89a..0390725 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -39,4 +39,6 @@ Search Race Gender + Description: + History: \ No newline at end of file