diff --git a/app/src/main/java/com/pixelized/rplexicon/business/SearchUseCase.kt b/app/src/main/java/com/pixelized/rplexicon/business/SearchUseCase.kt new file mode 100644 index 0000000..0aa350b --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/business/SearchUseCase.kt @@ -0,0 +1,292 @@ +package com.pixelized.rplexicon.business + +import android.content.Context +import androidx.compose.ui.text.AnnotatedString +import com.pixelized.rplexicon.R +import com.pixelized.rplexicon.data.model.Lexicon +import com.pixelized.rplexicon.data.model.Location +import com.pixelized.rplexicon.data.model.Quest +import com.pixelized.rplexicon.ui.screens.search.item.SearchItemUio +import com.pixelized.rplexicon.ui.screens.search.item.SearchItemUio.LocationSearchItemUio +import com.pixelized.rplexicon.ui.theme.typography.LexiconTypography +import com.pixelized.rplexicon.utilitary.annotate +import com.pixelized.rplexicon.utilitary.dropCapRegex +import com.pixelized.rplexicon.utilitary.extentions.prefix +import com.pixelized.rplexicon.utilitary.extractSentenceRegex +import com.pixelized.rplexicon.utilitary.extractWordRegex +import com.pixelized.rplexicon.utilitary.highlightRegex +import com.pixelized.rplexicon.utilitary.nullableAnnotate +import com.pixelized.rplexicon.utilitary.styleWith +import javax.inject.Inject + +class SearchUseCase @Inject constructor() { + + fun filterLexicon( + context: Context, + typography: LexiconTypography, + lexicon: List, + criterion: List, + ): List { + val dropCapRegex = dropCapRegex() + val highlightRegex = highlightRegex(terms = criterion) + val extractWordRegex = extractWordRegex(terms = criterion) + val extractSentenceRegex = extractSentenceRegex(terms = criterion) + + val prefixCategory = context.getString(R.string.search_category_prefix_lexicon) + val statusPrefix = context.getString(R.string.search_lexicon_item_status) + val locationPrefix = context.getString(R.string.search_lexicon_item_location) + val descriptionPrefix = context.getString(R.string.search_lexicon_item_description) + val historyPrefix = context.getString(R.string.search_lexicon_item_history) + val tagsPrefix = context.getString(R.string.search_lexicon_item_tags) + + return lexicon.filter { item -> + criterion.map { criteria -> + val category = item.category?.contains(criteria, true) == 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 status = item.status?.contains(criteria, true) == true + val location = item.location?.contains(criteria, true) == true + val description = item.description?.contains(criteria, true) == true + val history = item.history?.contains(criteria, true) == true + val tag = item.tags?.contains(criteria, true) == true + category || name || gender || race || diminutive || status || location || description || history || tag + }.all { it } + }.map { item -> + SearchItemUio.LexiconSearchItemUio( + id = item.id, + category = nullableAnnotate( + text = item.category?.prefix(prefixCategory), + highlightRegex styleWith typography.search.categoryHighlight, + ), + name = annotate( + text = item.name, + highlightRegex styleWith typography.search.titleHighlight, + dropCapRegex styleWith typography.titleMediumDropCap, + ), + diminutive = nullableAnnotate( + text = item.diminutive, + highlightRegex styleWith typography.search.extractHighlight, + ), + gender = nullableAnnotate( + text = item.gender, + highlightRegex styleWith typography.search.extractHighlight, + ), + race = nullableAnnotate( + text = item.race, + highlightRegex styleWith typography.search.extractHighlight, + ), + status = item.status?.let { extractWordRegex.find(it) }?.let { + AnnotatedString( + text = "$statusPrefix ", + spanStyle = typography.search.extractBold, + ) + annotate( + text = it.value, + highlightRegex styleWith typography.search.extractHighlight, + ) + }, + location = item.location?.let { extractWordRegex.find(it) }?.let { + AnnotatedString( + text = "$locationPrefix ", + spanStyle = typography.search.extractBold, + ) + annotate( + text = it.value, + highlightRegex styleWith typography.search.extractHighlight, + ) + }, + story = item.history?.let { extractSentenceRegex.find(it) }?.let { + AnnotatedString( + text = "$historyPrefix ", + spanStyle = typography.search.extractBold + ) + annotate( + text = it.value, + highlightRegex styleWith typography.search.extractHighlight, + ) + }, + description = item.description?.let { extractSentenceRegex.find(it) }?.let { + AnnotatedString( + text = "$descriptionPrefix ", + spanStyle = typography.search.extractBold, + ) + annotate( + text = it.value, + highlightRegex styleWith typography.search.extractHighlight, + ) + }, + tags = item.tags?.let { extractWordRegex.find(it) }?.let { + AnnotatedString( + text = "$tagsPrefix ", + spanStyle = typography.search.extractBold, + ) + annotate( + text = it.value, + highlightRegex styleWith typography.search.extractHighlight, + ) + }, + ) + } + } + + fun filterQuests( + context: Context, + typography: LexiconTypography, + quests: List, + criterion: List, + ): List { + val dropCapRegex = dropCapRegex() + val highlightRegex = highlightRegex(terms = criterion) + val extractSentence = extractSentenceRegex(terms = criterion) + + val prefixCategory = context.getString(R.string.search_category_prefix_quest) + val ownerPrefix = context.getString(R.string.search_quest_item_owner) + val locationPrefix = context.getString(R.string.search_quest_item_location) + val individualRewardPrefix = context.getString(R.string.search_quest_item_individualReward) + val groupRewardPrefix = context.getString(R.string.search_quest_item_groupReward) + val descriptionPrefix = context.getString(R.string.search_quest_item_description) + + return quests.filter { item -> + criterion.map { criteria -> + val category = item.category?.contains(criteria, true) == true + val name = item.title.contains(criteria, true) + val entry = item.entries.any { + val title = it.subtitle?.contains(criteria, true) == true + val subTitle = it.subtitle?.contains(criteria, true) == true + val questGiver = it.questGiver?.contains(criteria, true) == true + val location = it.area?.contains(criteria, true) == true + val individualReward = it.individualReward?.contains(criteria, true) == true + val groupReward = it.groupReward?.contains(criteria, true) == true + val description = it.description.contains(criteria, true) + title || subTitle || questGiver || location || individualReward || groupReward || description + } + category || name || entry + }.all { it } + }.map { item -> + val entry = item.entries.firstOrNull { + criterion.map { criteria -> + val title = it.subtitle?.contains(criteria, true) == true + val subTitle = it.subtitle?.contains(criteria, true) == true + val questGiver = it.questGiver?.contains(criteria, true) == true + val location = it.area?.contains(criteria, true) == true + val individualReward = it.individualReward?.contains(criteria, true) == true + val groupReward = it.groupReward?.contains(criteria, true) == true + val description = it.description.contains(criteria, true) + title || subTitle || questGiver || location || individualReward || groupReward || description + }.all { it } + } + SearchItemUio.QuestSearchItemUio( + id = item.id, + category = nullableAnnotate( + text = item.category?.prefix(prefixCategory), + highlightRegex styleWith typography.search.categoryHighlight, + ), + title = annotate( + text = item.title, + highlightRegex styleWith typography.search.titleHighlight, + dropCapRegex styleWith typography.titleMediumDropCap, + ), + owner = entry?.questGiver?.let { + AnnotatedString( + text = "$ownerPrefix ", + spanStyle = typography.search.extractBold, + ) + annotate( + text = it, + highlightRegex styleWith typography.search.extractHighlight, + ) + }, + location = entry?.area?.let { + AnnotatedString( + text = "$locationPrefix ", + spanStyle = typography.search.extractBold, + ) + annotate( + text = it, + highlightRegex styleWith typography.search.extractHighlight, + ) + }, + individualReward = entry?.individualReward?.let { + AnnotatedString( + text = "$individualRewardPrefix ", + spanStyle = typography.search.extractBold, + ) + annotate( + text = it, + highlightRegex styleWith typography.search.extractHighlight, + ) + }, + groupReward = entry?.groupReward?.let { + AnnotatedString( + text = "$groupRewardPrefix ", + spanStyle = typography.search.extractBold, + ) + annotate( + text = it, + highlightRegex styleWith typography.search.extractHighlight, + ) + }, + description = entry?.description?.let { extractSentence.find(it) }?.let { + AnnotatedString( + text = "$descriptionPrefix ", + spanStyle = typography.search.extractBold + ) + annotate( + text = it.value, + highlightRegex styleWith typography.search.extractHighlight, + ) + }, + ) + } + } + + fun filterLocation( + context: Context, + typography: LexiconTypography, + locations: List, + criterion: List, + ): List { + val dropCapRegex = dropCapRegex() + val highlightRegex = highlightRegex(terms = criterion) + val extractSentence = extractSentenceRegex(terms = criterion) + + val prefixCategory = context.getString(R.string.search_category_prefix_location) + val descriptionPrefix = context.getString(R.string.search_location_item_description) + val destinationPrefix = context.getString(R.string.search_location_item_destination) + + return locations.filter { item -> + criterion.map { criteria -> + val category = item.category?.contains(criteria, true) == true + val name = item.name.contains(criteria, true) + val description = item.description?.contains(criteria, true) == true + val child = item.child.any { it.second.name.contains(criteria, true) } + category || name || description || child + }.all { it } + }.map { item -> + LocationSearchItemUio( + id = item.id, + category = nullableAnnotate( + text = item.category?.prefix(prefixCategory), + highlightRegex styleWith typography.search.categoryHighlight, + ), + title = annotate( + text = item.name, + highlightRegex styleWith typography.search.titleHighlight, + dropCapRegex styleWith typography.titleMediumDropCap, + ), + description = item.description?.let { extractSentence.find(it) }?.let { + AnnotatedString( + text = "$descriptionPrefix ", + spanStyle = typography.search.extractBold, + ) + annotate( + text = it.value, + highlightRegex styleWith typography.search.extractHighlight, + ) + }, + destination = item.child.mapNotNull { child -> + highlightRegex.find(child.second.name)?.let { child.second.name } + }.takeIf { it.any() }?.let { + AnnotatedString( + text = "$destinationPrefix ", + spanStyle = typography.search.extractBold, + ) + annotate( + text = it.joinToString { name -> name }, + highlightRegex styleWith typography.search.extractHighlight, + ) + } + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/composable/CategoryHeader.kt b/app/src/main/java/com/pixelized/rplexicon/ui/composable/CategoryHeader.kt index 790b0e3..c68132d 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/composable/CategoryHeader.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/composable/CategoryHeader.kt @@ -42,7 +42,7 @@ fun CategoryHeader( @Stable @Composable -private fun rememberHorizontalGradient(): Brush { +fun rememberHorizontalGradient(): Brush { val colorScheme = MaterialTheme.colorScheme return remember { Brush.horizontalGradient( diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/composable/form/TextField.kt b/app/src/main/java/com/pixelized/rplexicon/ui/composable/form/TextFieldAppBar.kt similarity index 73% rename from app/src/main/java/com/pixelized/rplexicon/ui/composable/form/TextField.kt rename to app/src/main/java/com/pixelized/rplexicon/ui/composable/form/TextFieldAppBar.kt index 1f37477..a2267fe 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/composable/form/TextField.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/composable/form/TextFieldAppBar.kt @@ -7,6 +7,7 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.KeyboardActions @@ -14,7 +15,6 @@ import androidx.compose.foundation.text.KeyboardOptions 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 @@ -24,6 +24,7 @@ import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -41,41 +42,58 @@ import com.pixelized.rplexicon.utilitary.extentions.lexicon data class TextFieldUio( @StringRes val label: Int, val value: State, - val onValueChange: (String) -> Unit, ) { companion object { - fun preview(@StringRes label: Int) = TextFieldUio( + fun preview(@StringRes label: Int, value: String = "") = TextFieldUio( label = label, - value = mutableStateOf(""), - onValueChange = {}, + value = mutableStateOf(value), ) } } @Composable -fun TextField( +fun TextFieldAppBar( modifier: Modifier = Modifier, field: TextFieldUio, + onBack: () -> Unit, + onClear: () -> Unit, + onValueChange: (String) -> Unit, ) { val focus = LocalFocusManager.current - OutlinedTextField( - modifier = modifier, + androidx.compose.material3.TextField( + modifier = modifier.height(height = 64.dp), shape = MaterialTheme.lexicon.shapes.textField, colors = TextFieldDefaults.colors( - focusedContainerColor = MaterialTheme.colorScheme.surface, - unfocusedContainerColor = MaterialTheme.colorScheme.surface, + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, ), label = { Text(text = stringResource(id = field.label)) }, + leadingIcon = { + IconButton( + modifier = Modifier.padding(all = 4.dp), + onClick = onBack, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_arrow_back_ios_new_24), + contentDescription = null + ) + } + }, trailingIcon = { AnimatedVisibility( visible = field.value.value.isNotEmpty(), enter = fadeIn(), exit = fadeOut(), ) { - IconButton(onClick = { field.onValueChange("") }) { + IconButton( + modifier = Modifier.padding(all = 4.dp), + onClick = onClear, + ) { Icon( modifier = Modifier.size(size = 18.dp), painter = painterResource(id = R.drawable.ic_clear_24), @@ -93,7 +111,7 @@ fun TextField( ), maxLines = 1, value = field.value.value, - onValueChange = field.onValueChange, + onValueChange = onValueChange, ) } @@ -105,15 +123,17 @@ private fun TextFieldPreview( ) { LexiconTheme { Surface { - TextField( + TextFieldAppBar( modifier = Modifier .fillMaxWidth() .padding(all = 8.dp), field = TextFieldUio( label = R.string.lexicon_search, value = remember { mutableStateOf(preview) }, - onValueChange = {}, - ) + ), + onBack = { }, + onClear = { }, + onValueChange = { }, ) } } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/ScreenNavHost.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/ScreenNavHost.kt index 8d43175..1aa0a41 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/ScreenNavHost.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/ScreenNavHost.kt @@ -15,7 +15,7 @@ import com.pixelized.rplexicon.ui.navigation.screens.composableCharacterSheet import com.pixelized.rplexicon.ui.navigation.screens.composableHome import com.pixelized.rplexicon.ui.navigation.screens.composableLanding import com.pixelized.rplexicon.ui.navigation.screens.composableLexiconDetail -import com.pixelized.rplexicon.ui.navigation.screens.composableLexiconSearch +import com.pixelized.rplexicon.ui.navigation.screens.composableSearch import com.pixelized.rplexicon.ui.navigation.screens.composableLocationDetail import com.pixelized.rplexicon.ui.navigation.screens.composableQuestDetail import com.pixelized.rplexicon.ui.navigation.screens.composableSpellDetail @@ -53,7 +53,7 @@ fun ScreenNavHost( ) composableLanding() composableLexiconDetail() - composableLexiconSearch() + composableSearch() composableQuestDetail() composableLocationDetail() composableCharacterSheet() diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableLexiconDetail.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableLexiconDetail.kt index efb264f..120f221 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableLexiconDetail.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableLexiconDetail.kt @@ -17,9 +17,7 @@ private const val ROUTE = "LexiconDetail" private const val ARG_ID = "id" private const val ARG_HIGHLIGHT = "highlight" -val LEXICON_DETAIL_ROUTE = ROUTE + - "?${ARG_ID.ARG}" + - "&${ARG_HIGHLIGHT.ARG}" +val LEXICON_DETAIL_ROUTE = "$ROUTE?${ARG_ID.ARG}&${ARG_HIGHLIGHT.ARG}" @Stable @Immutable @@ -57,9 +55,6 @@ fun NavHostController.navigateToLexiconDetail( highlight: String? = null, option: NavOptionsBuilder.() -> Unit = {}, ) { - val route = ROUTE + - "?$ARG_ID=$id" + - "&$ARG_HIGHLIGHT=$highlight" - + val route = "$ROUTE?$ARG_ID=$id&$ARG_HIGHLIGHT=$highlight" navigate(route = route, builder = option) } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableLexiconSearch.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableLexiconSearch.kt index 36c0fc3..ba2cd50 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableLexiconSearch.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableLexiconSearch.kt @@ -1,27 +1,76 @@ package com.pixelized.rplexicon.ui.navigation.screens +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.NavOptionsBuilder +import androidx.navigation.NavType +import androidx.navigation.navArgument import com.pixelized.rplexicon.ui.navigation.NavigationAnimation import com.pixelized.rplexicon.ui.navigation.animatedComposable -import com.pixelized.rplexicon.ui.screens.lexicon.search.SearchScreen +import com.pixelized.rplexicon.ui.screens.search.SearchScreen +import com.pixelized.rplexicon.utilitary.extentions.ARG private const val ROUTE = "search" +private const val ARG_ENABLE_LEXICON = "ARG_ENABLE_LEXICON" +private const val ARG_ENABLE_QUESTS = "ARG_ENABLE_QUESTS" +private const val ARG_ENABLE_LOCATIONS = "ARG_ENABLE_LOCATIONS" -const val SEARCH_ROUTE = ROUTE +val SEARCH_ROUTE = ROUTE + + "?${ARG_ENABLE_LEXICON.ARG}" + + "&${ARG_ENABLE_QUESTS.ARG}" + + "&${ARG_ENABLE_LOCATIONS.ARG}" -fun NavGraphBuilder.composableLexiconSearch() { +@Stable +@Immutable +data class SearchArgument( + val enableLexicon: Boolean = false, + val enableQuests: Boolean = false, + val enableLocations: Boolean = false, +) + +val SavedStateHandle.searchArgument: SearchArgument + get() = SearchArgument( + enableLexicon = get(ARG_ENABLE_LEXICON) ?: false, + enableQuests = get(ARG_ENABLE_QUESTS) ?: false, + enableLocations = get(ARG_ENABLE_LOCATIONS) ?: false, + ) + +fun NavGraphBuilder.composableSearch() { animatedComposable( route = SEARCH_ROUTE, + arguments = listOf( + navArgument(ARG_ENABLE_LEXICON) { + type = NavType.BoolType + nullable = false + }, + navArgument(ARG_ENABLE_QUESTS) { + type = NavType.BoolType + nullable = false + }, + navArgument(ARG_ENABLE_LOCATIONS) { + type = NavType.BoolType + nullable = false + }, + ), animation = NavigationAnimation.Push, ) { SearchScreen() } } -fun NavHostController.navigateToLexiconSearch( +fun NavHostController.navigateToSearch( option: NavOptionsBuilder.() -> Unit = {}, + enableLexicon: Boolean = false, + enableQuests: Boolean = false, + enableLocations: Boolean = false, ) { - navigate(route = ROUTE, builder = option) + val route = ROUTE + + "?$ARG_ENABLE_LEXICON=$enableLexicon" + + "&$ARG_ENABLE_QUESTS=$enableQuests" + + "&$ARG_ENABLE_LOCATIONS=$enableLocations" + + navigate(route = route, builder = option) } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableLocationDetail.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableLocationDetail.kt index be15608..234bb50 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableLocationDetail.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableLocationDetail.kt @@ -10,14 +10,14 @@ import androidx.navigation.NavType import androidx.navigation.navArgument import com.pixelized.rplexicon.ui.navigation.NavigationAnimation import com.pixelized.rplexicon.ui.navigation.animatedComposable -import com.pixelized.rplexicon.ui.screens.location.detail.LocationDetail +import com.pixelized.rplexicon.ui.screens.location.detail.LocationDetailScreen import com.pixelized.rplexicon.utilitary.extentions.ARG private const val ROUTE = "LocationDetail" private const val ARG_ID = "id" +private const val ARG_HIGHLIGHT = "highlight" -val LOCATION_DETAIL_ROUTE = ROUTE + - "?${ARG_ID.ARG}" +val LOCATION_DETAIL_ROUTE = "$ROUTE?${ARG_ID.ARG}&${ARG_HIGHLIGHT.ARG}" @Stable @Immutable @@ -37,19 +37,22 @@ fun NavGraphBuilder.composableLocationDetail() { navArgument(name = ARG_ID) { type = NavType.StringType }, + navArgument(name = ARG_HIGHLIGHT) { + type = NavType.StringType + nullable = true + }, ), animation = NavigationAnimation.Push, ) { - LocationDetail() + LocationDetailScreen() } } fun NavHostController.navigateToLocationDetail( id: String, + highlight: String? = null, option: NavOptionsBuilder.() -> Unit = {}, ) { - val route = ROUTE + - "?$ARG_ID=$id" - + val route = "$ROUTE?$ARG_ID=$id&$ARG_HIGHLIGHT=$highlight" navigate(route = route, builder = option) } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableQuestDetail.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableQuestDetail.kt index db1b42b..794eae4 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableQuestDetail.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableQuestDetail.kt @@ -15,8 +15,9 @@ import com.pixelized.rplexicon.utilitary.extentions.ARG private const val ROUTE = "QuestDetail" private const val ARG_ID = "id" +private const val ARG_HIGHLIGHT = "highlight" -val QUEST_DETAIL_ROUTE = "$ROUTE?${ARG_ID.ARG}" +val QUEST_DETAIL_ROUTE = "$ROUTE?${ARG_ID.ARG}&${ARG_HIGHLIGHT.ARG}" @Stable @Immutable @@ -36,6 +37,10 @@ fun NavGraphBuilder.composableQuestDetail() { navArgument(name = ARG_ID) { type = NavType.StringType }, + navArgument(name = ARG_HIGHLIGHT) { + type = NavType.StringType + nullable = true + }, ), animation = NavigationAnimation.Push, ) { @@ -45,9 +50,9 @@ fun NavGraphBuilder.composableQuestDetail() { fun NavHostController.navigateToQuestDetail( id: String, + highlight: String? = null, option: NavOptionsBuilder.() -> Unit = {}, ) { - val route = "$ROUTE?$ARG_ID=$id" - + val route = "$ROUTE?$ARG_ID=$id&$ARG_HIGHLIGHT=$highlight" navigate(route = route, builder = option) } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/detail/LexiconDetailScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/detail/LexiconDetailScreen.kt index 9f8914e..5ab170e 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/detail/LexiconDetailScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/detail/LexiconDetailScreen.kt @@ -1,5 +1,6 @@ package com.pixelized.rplexicon.ui.screens.lexicon.detail +import android.content.res.Configuration import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.net.Uri @@ -32,22 +33,20 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocal +import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalView 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.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -62,12 +61,13 @@ import com.pixelized.rplexicon.ui.composable.FullScreenImageViewModel import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost import com.pixelized.rplexicon.ui.navigation.screens.navigateToCharacterSheet import com.pixelized.rplexicon.ui.theme.LexiconTheme -import com.pixelized.rplexicon.utilitary.extentions.annotatedSpan -import com.pixelized.rplexicon.utilitary.extentions.annotatedString -import com.pixelized.rplexicon.utilitary.extentions.highlightRegex +import com.pixelized.rplexicon.utilitary.annotate +import com.pixelized.rplexicon.utilitary.dropCapRegex import com.pixelized.rplexicon.utilitary.extentions.lexicon import com.pixelized.rplexicon.utilitary.extentions.scrollOffset import com.pixelized.rplexicon.utilitary.extentions.searchCriterion +import com.pixelized.rplexicon.utilitary.highlightRegex +import com.pixelized.rplexicon.utilitary.styleWith @Stable data class LexiconDetailUio( @@ -80,77 +80,9 @@ data class LexiconDetailUio( val portrait: List, val description: String?, val history: String?, - val search: String?, val tags: String?, ) -@Stable -data class AnnotatedLexiconDetailUio( - val name: AnnotatedString, - val diminutive: AnnotatedString?, - val gender: AnnotatedString?, - val race: AnnotatedString?, - val portrait: List, - val status: AnnotatedString?, - val location: AnnotatedString?, - val description: AnnotatedString?, - val history: AnnotatedString?, - val tags: AnnotatedString? -) - -@Composable -@Stable -fun LexiconDetailUio.annotate(): AnnotatedLexiconDetailUio { - val colorScheme = MaterialTheme.colorScheme - val typography = MaterialTheme.lexicon.typography - val highlight = remember { SpanStyle(color = colorScheme.primary) } - val trimmedSearch = remember(search) { search.searchCriterion() } - val highlightRegex = remember(search) { trimmedSearch.highlightRegex } - - return remember(search) { - AnnotatedLexiconDetailUio( - portrait = portrait, - name = AnnotatedString( - text = name, - spanStyles = (highlightRegex?.annotatedSpan( - input = name, - spanStyle = highlight, - ) ?: emptyList()) + listOf( - AnnotatedString.Range( - item = typography.headlineSmallDropCap, - start = 0, - end = 1, - ) - ) - ), - diminutive = diminutive?.let { - highlightRegex.annotatedString(input = it, spanStyle = highlight) - }, - gender = gender?.let { gender -> - highlightRegex.annotatedString(input = gender, spanStyle = highlight) - }, - race = race?.let { race -> - highlightRegex.annotatedString(input = race, spanStyle = highlight) - }, - description = description?.let { description -> - highlightRegex.annotatedString(input = description, spanStyle = highlight) - }, - status = status?.let { status -> - highlightRegex.annotatedString(input = status, spanStyle = highlight) - }, - location = location?.let { location -> - highlightRegex.annotatedString(input = location, spanStyle = highlight) - }, - history = history?.let { history -> - highlightRegex.annotatedString(input = history, spanStyle = highlight) - }, - tags = tags?.let { tags -> - highlightRegex.annotatedString(input = tags, spanStyle = highlight) - }, - ) - } -} - @Composable fun LexiconDetailScreen( viewModel: LexiconDetailViewModel = hiltViewModel(), @@ -164,6 +96,7 @@ fun LexiconDetailScreen( LexiconDetailContent( modifier = Modifier.fillMaxSize(), item = viewModel.character, + highlight = viewModel.highlight, haveCharacterSheet = viewModel.haveCharacterSheet, onBack = { screen.popBackStack() }, onCharacterSheet = { screen.navigateToCharacterSheet(name = it) }, @@ -182,16 +115,19 @@ private fun LexiconDetailContent( modifier: Modifier = Modifier, state: ScrollState = rememberScrollState(), item: State, + highlight: String?, haveCharacterSheet: State, onBack: () -> Unit, onCharacterSheet: (String) -> Unit, onImage: (Uri) -> Unit, ) { - val colorScheme = MaterialTheme.colorScheme - val typography = MaterialTheme.typography - val annotatedItem = item.value?.annotate() - val backgroundUri = remember(annotatedItem) { - annotatedItem?.portrait?.firstOrNull() + val typography = MaterialTheme.lexicon.typography + val highlightRegex = remember(highlight) { highlightRegex(terms = highlight.searchCriterion()) } + val dropCapRegex = remember { dropCapRegex() } + + val item = item.value + val backgroundUri = remember(item) { + item?.portrait?.firstOrNull() } Scaffold( @@ -210,7 +146,7 @@ private fun LexiconDetailContent( }, actions = { AnimatedVisibility(visible = haveCharacterSheet.value) { - IconButton(onClick = { item.value?.name?.let(onCharacterSheet) }) { + IconButton(onClick = { item?.name?.let(onCharacterSheet) }) { Icon( painter = painterResource(id = R.drawable.ic_d20_24), contentDescription = null @@ -252,19 +188,26 @@ private fun LexiconDetailContent( modifier = Modifier.padding(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(4.dp) ) { - annotatedItem?.name?.let { + item?.name?.let { Text( modifier = Modifier.alignByBaseline(), - style = typography.headlineSmall, - text = it, + style = typography.base.headlineSmall, + text = annotate( + text = it, + dropCapRegex styleWith typography.headlineSmallDropCap, + highlightRegex styleWith typography.detail.highlightStyle, + ), ) } - annotatedItem?.diminutive?.let { + item?.diminutive?.let { Text( modifier = Modifier.alignByBaseline(), - style = typography.labelMedium, + style = typography.base.labelMedium, fontStyle = FontStyle.Italic, - text = it, + text = annotate( + text = it, + highlightRegex styleWith typography.detail.highlightStyle, + ), ) } } @@ -273,123 +216,122 @@ private fun LexiconDetailContent( modifier = Modifier.padding(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(4.dp) ) { - annotatedItem?.gender?.let { + item?.gender?.let { Text( - style = typography.labelMedium, + style = typography.base.labelMedium, fontStyle = FontStyle.Italic, - text = it, + text = annotate( + text = it, + highlightRegex styleWith typography.detail.highlightStyle, + ), ) } - annotatedItem?.race?.let { + item?.race?.let { Text( - style = typography.labelMedium, + style = typography.base.labelMedium, fontStyle = FontStyle.Italic, - text = it, + text = annotate( + text = it, + highlightRegex styleWith typography.detail.highlightStyle, + ), ) } } - if (annotatedItem?.status != null || annotatedItem?.location != null) { + if (item?.status != null || item?.location != null) { Spacer(modifier = Modifier.height(8.dp)) } - annotatedItem?.status?.let { + item?.status?.let { Row( modifier = Modifier.padding(start = 16.dp, end = 16.dp), horizontalArrangement = Arrangement.spacedBy(space = 4.dp), ) { Text( modifier = Modifier.alignByBaseline(), - style = typography.bodyMedium, + style = typography.base.bodyMedium, fontWeight = FontWeight.Bold, text = stringResource(id = R.string.detail_status), ) Text( modifier = Modifier.alignByBaseline(), - style = remember { - typography.bodyMedium.copy( - shadow = Shadow( - color = colorScheme.surface.copy(alpha = 0.5f), - offset = Offset(x = 1f, y = 1f), - ) - ) - }, - text = it, + style = typography.base.bodyMedium, + text = annotate( + text = it, + highlightRegex styleWith typography.detail.highlightStyle, + ), ) } } - annotatedItem?.location?.let { + item?.location?.let { Row( modifier = Modifier.padding(start = 16.dp, end = 16.dp), horizontalArrangement = Arrangement.spacedBy(space = 4.dp), ) { Text( modifier = Modifier.alignByBaseline(), - style = typography.bodyMedium, + style = typography.base.bodyMedium, fontWeight = FontWeight.Bold, text = stringResource(id = R.string.detail_location), ) Text( modifier = Modifier.alignByBaseline(), - style = remember { - typography.bodyMedium.copy( - shadow = Shadow( - color = colorScheme.surface.copy(alpha = 0.5f), - offset = Offset(x = 1f, y = 1f), - ) - ) - }, - text = it, + style = typography.base.bodyMedium, + text = annotate( + text = it, + highlightRegex styleWith typography.detail.highlightStyle, + ), ) } } - annotatedItem?.description?.let { + item?.description?.let { Text( modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp), - style = typography.titleMedium, + style = typography.base.titleMedium, text = stringResource(id = R.string.detail_description), ) Text( modifier = Modifier.padding(horizontal = 16.dp), - style = remember { - typography.bodyMedium.copy( - shadow = Shadow( - color = colorScheme.surface.copy(alpha = 0.5f), - offset = Offset(x = 1f, y = 1f), - ) - ) - }, - text = it, + style = typography.base.bodyMedium, + text = annotate( + text = it, + dropCapRegex styleWith typography.bodyMediumDropCap, + highlightRegex styleWith typography.detail.highlightStyle, + ), ) } - annotatedItem?.history?.let { + item?.history?.let { Text( modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp), - style = typography.titleMedium, + style = typography.base.titleMedium, text = stringResource(id = R.string.detail_history), ) Text( modifier = Modifier.padding(horizontal = 16.dp), - style = typography.bodyMedium, - text = it, + style = typography.base.bodyMedium, + text = annotate( + text = it, + dropCapRegex styleWith typography.bodyMediumDropCap, + highlightRegex styleWith typography.detail.highlightStyle, + ), ) } - if (annotatedItem?.portrait?.isNotEmpty() == true) { + if (item?.portrait?.isNotEmpty() == true) { val maxSize = rememberPortraitWidth() Text( modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp), - style = typography.titleMedium, + style = typography.base.titleMedium, text = stringResource(id = R.string.detail_portrait), ) LazyRow( contentPadding = PaddingValues(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - items(items = annotatedItem.portrait) { + items(items = item.portrait) { AsyncImage( modifier = Modifier .clickable { onImage(it) } @@ -406,12 +348,15 @@ private fun LexiconDetailContent( } } - annotatedItem?.tags?.let { + item?.tags?.let { Text( modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp), - style = typography.labelSmall, + style = typography.base.labelSmall, fontStyle = FontStyle.Italic, - text = it, + text = annotate( + text = it, + highlightRegex styleWith typography.detail.highlightStyle, + ), ) } } @@ -453,13 +398,13 @@ private fun LexiconDetailPreview() { 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, tags = "protagoniste, brute", - search = "Bru", ) ) } LexiconDetailContent( modifier = Modifier.fillMaxSize(), item = character, + highlight = "Bru poule faible", haveCharacterSheet = remember { mutableStateOf(true) }, onBack = { }, onCharacterSheet = { }, diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/detail/LexiconDetailViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/detail/LexiconDetailViewModel.kt index 075ba1d..564a707 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/detail/LexiconDetailViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/detail/LexiconDetailViewModel.kt @@ -21,10 +21,11 @@ class LexiconDetailViewModel @Inject constructor( private val _character = mutableStateOf(null) val character: State get() = _character + val highlight: String? = savedStateHandle.lexiconDetailArgument.highlight + init { val argument = savedStateHandle.lexiconDetailArgument val source = lexiconRepository.find(id = argument.id) - if (source != null) { _character.value = LexiconDetailUio( name = source.name, @@ -37,7 +38,6 @@ class LexiconDetailViewModel @Inject constructor( description = source.description, history = source.history, tags = source.tags, - search = argument.highlight, ) } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/list/LexiconScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/list/LexiconScreen.kt index 105a17e..674f307 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/list/LexiconScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/list/LexiconScreen.kt @@ -2,7 +2,7 @@ package com.pixelized.rplexicon.ui.screens.lexicon.list import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_YES -import androidx.compose.animation.AnimatedContent +import android.util.Log import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -43,10 +43,11 @@ import com.pixelized.rplexicon.ui.composable.Loader import com.pixelized.rplexicon.ui.composable.error.HandleFetchError import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost import com.pixelized.rplexicon.ui.navigation.screens.navigateToLexiconDetail -import com.pixelized.rplexicon.ui.navigation.screens.navigateToLexiconSearch +import com.pixelized.rplexicon.ui.navigation.screens.navigateToSearch import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.utilitary.extentions.cell import com.pixelized.rplexicon.utilitary.extentions.lexicon +import com.pixelized.rplexicon.utilitary.extentions.scroll import kotlinx.coroutines.launch @@ -68,7 +69,7 @@ fun LexiconScreen( ) val isFabExpended = remember { derivedStateOf { - lazyListState.canScrollForward.not() && viewModel.items.value.isNotEmpty() + viewModel.items.value.isNotEmpty() && lazyListState.scroll && lazyListState.canScrollForward.not() } } @@ -80,7 +81,7 @@ fun LexiconScreen( refreshing = viewModel.isLoading, isFabExpended = isFabExpended, onSearch = { - screen.navigateToLexiconSearch() + screen.navigateToSearch(enableLexicon = true) }, onItem = { screen.navigateToLexiconDetail(id = it.id) @@ -100,68 +101,45 @@ private fun LexiconScreenContent( lazyColumnState: LazyListState, refreshState: PullRefreshState, refreshing: State, - isFabExpended: State, items: State>, + isFabExpended: State, onSearch: () -> Unit, onItem: (LexiconItemUio) -> Unit, ) { Box( modifier = modifier, ) { - AnimatedContent( - targetState = items.value.isEmpty(), - transitionSpec = MaterialTheme.lexicon.animation.itemList, - label = "AnimatedLexicon" - ) { empty -> - when (empty) { - true -> LazyColumn( - modifier = Modifier - .fillMaxSize() - .pullRefresh(state = refreshState), - state = lazyColumnState, - contentPadding = MaterialTheme.lexicon.dimens.itemListPadding, - ) { - items(count = 16) { - LexiconItem( - modifier = Modifier.cell(), - item = LexiconItemUio.placeholder(), + LazyColumn( + modifier = Modifier + .fillMaxSize() + .pullRefresh(state = refreshState), + state = lazyColumnState, + contentPadding = MaterialTheme.lexicon.dimens.itemListPadding, + ) { + items.value.forEachIndexed { index, entry -> + entry.category?.let { + item( + contentType = { "Header" }, + ) { + CategoryHeader( + modifier = Modifier + .padding(top = if (index == 0) 0.dp else 16.dp) + .padding(horizontal = 16.dp), + text = it, ) } } - - else -> LazyColumn( - modifier = Modifier - .fillMaxSize() - .pullRefresh(state = refreshState), - state = lazyColumnState, - contentPadding = MaterialTheme.lexicon.dimens.itemListPadding, + items( + items = entry.items, + key = { it.id }, + contentType = { "Lexicon" }, ) { - items.value.forEachIndexed { index, entry -> - entry.category?.let { - item( - contentType = { "Header" }, - ) { - CategoryHeader( - modifier = Modifier - .padding(top = if (index == 0) 0.dp else 16.dp) - .padding(horizontal = 16.dp), - text = it, - ) - } - } - items( - items = entry.items, - key = { it.id }, - contentType = { "Lexicon" }, - ) { - LexiconItem( - modifier = Modifier - .clickable { onItem(it) } - .cell(), - item = it, - ) - } - } + LexiconItem( + modifier = Modifier + .clickable { onItem(it) } + .cell(), + item = it, + ) } } } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/search/SearchItem.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/search/SearchItem.kt deleted file mode 100644 index 8a27b6d..0000000 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/search/SearchItem.kt +++ /dev/null @@ -1,324 +0,0 @@ -package com.pixelized.rplexicon.ui.screens.lexicon.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.Row -import androidx.compose.foundation.layout.fillMaxWidth -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.ui.theme.LexiconTheme -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.extentions.lexicon -import com.pixelized.rplexicon.utilitary.extentions.searchCriterion - -@Stable -class SearchItemUio( - val id: String, - val name: String, - val diminutive: String?, - val gender: String?, - val race: String?, - val status: String?, - val location: String?, - val description: String?, - val history: String?, - val tags: String?, - val search: String, -) { - companion object { - fun preview( - id: String = "Brulkhai-1", - name: String = "Brulkhai", - diminutive: String? = "Bru", - gender: String? = "Female", - race: String? = "Half-Orc", - description: String? = "Brulkhai, ou plus simplement Bru, est une demi-orc de 38 ans solidement bâti. Elle mesure 192 cm pour 110 kg de muscles lorsqu’elle est en bonne santé. Elle a les cheveux châtains, les yeux noisettes et la peau couleur gris-vert typique de son espèce. D’un tempérament taciturne, elle parle peu et de façon concise. Elle est parfois brutale, aussi bien physiquement que verbalement, Elle ne prend cependant aucun plaisir à malmener ceux qu’elle considère plus faibles qu’elle.\n" + - "D’une nature simple et honnête, elle ne mâche pas ses mots et ne dissimule généralement pas ses pensées. Son intelligence modeste est plus le reflet d’un manque d’éducation et d’une capacité limitée à gérer ses émotions qu’à une débilité congénitale.\n" + - "Elle voue à la force un culte car c’est par son expression qu’elle se sent vraiment vivante et éprouve de grandes difficultés vis à vis de ceux qu’elle nomme foshnu (bébé, chouineur en commun).", - history: String? = null, - search: String = "", - ): SearchItemUio { - return SearchItemUio( - id = id, - name = name, - diminutive = diminutive, - gender = gender, - race = race, - status = null, - location = null, - description = description, - history = history, - tags = null, - search = search, - ) - } - } -} - -@Stable -class AnnotatedSearchItemUio( - val id: String, - val name: AnnotatedString, - val diminutive: AnnotatedString?, - val gender: AnnotatedString?, - val race: AnnotatedString?, - val status: AnnotatedString?, - val location: AnnotatedString?, - val description: AnnotatedString?, - val history: AnnotatedString?, - val tags: AnnotatedString?, -) - -@Composable -@Stable -private fun SearchItemUio.annotate(): AnnotatedSearchItemUio { - val colorScheme = MaterialTheme.colorScheme - val highlight = remember { SpanStyle(color = colorScheme.primary) } - val trimmedSearch = remember(search) { search.searchCriterion() } - val highlightRegex = remember(search) { trimmedSearch.highlightRegex } - val finderRegex = remember(search) { trimmedSearch.finderRegex } - - return remember(trimmedSearch) { - AnnotatedSearchItemUio( - id = id, - name = AnnotatedString( - text = name, - spanStyles = highlightRegex?.annotatedSpan( - input = name, - spanStyle = highlight, - ) ?: emptyList() - ), - diminutive = highlightRegex?.annotatedString( - input = diminutive ?: "", - spanStyle = highlight - ), - gender = finderRegex?.foldAll(gender)?.let { gender -> - highlightRegex?.annotatedString(gender, spanStyle = highlight) - }, - race = finderRegex?.foldAll(race)?.let { race -> - highlightRegex?.annotatedString(race, spanStyle = highlight) - }, - status = finderRegex?.foldAll(status)?.let { status -> - highlightRegex?.annotatedString(status, spanStyle = highlight) - }, - location = finderRegex?.foldAll(location)?.let { location -> - highlightRegex?.annotatedString(location, spanStyle = highlight) - }, - description = finderRegex?.foldAll(description)?.let { description -> - highlightRegex?.annotatedString(description, spanStyle = highlight) - }, - history = finderRegex?.foldAll(history)?.let { history -> - highlightRegex?.annotatedString(history, spanStyle = highlight) - }, - tags = finderRegex?.foldAll(tags)?.let { tags -> - highlightRegex?.annotatedString(tags, spanStyle = highlight) - } - ) - } -} - -@Composable -fun SearchItem( - modifier: Modifier = Modifier, - item: SearchItemUio, -) { - val typography = MaterialTheme.typography - val colorScheme = MaterialTheme.lexicon.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(space = 4.dp), - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(space = 4.dp), - ) { - Text( - modifier = Modifier.alignByBaseline(), - style = typography.bodyLarge, - fontWeight = FontWeight.Bold, - maxLines = 1, - text = annotatedItem.name, - ) - annotatedItem.diminutive?.let { - Text( - modifier = Modifier.alignByBaseline(), - style = typography.labelMedium, - maxLines = 1, - text = it, - ) - } - annotatedItem.gender?.let { - Text( - modifier = Modifier.alignByBaseline(), - style = typography.labelMedium, - fontStyle = FontStyle.Italic, - maxLines = 1, - text = it, - ) - } - annotatedItem.race?.let { - Text( - modifier = Modifier.alignByBaseline(), - style = typography.labelMedium, - fontStyle = FontStyle.Italic, - maxLines = 1, - text = it, - ) - } - } - - Column( - modifier = Modifier.drawBehind { - drawLine( - color = colorScheme.base.primary, - start = Offset(0f, 0f), - end = Offset(0f, size.height), - strokeWidth = 2.dp.toPx() - ) - }, - verticalArrangement = Arrangement.spacedBy(4.dp), - ) { - annotatedItem.status?.let { - Row( - modifier = Modifier.padding(start = 8.dp), - horizontalArrangement = Arrangement.spacedBy(space = 4.dp), - ) { - Text( - style = typography.labelSmall, - fontWeight = FontWeight.Bold, - text = stringResource(id = R.string.search_item_status), - ) - Text( - style = typography.labelSmall, - fontStyle = FontStyle.Italic, - text = it, - ) - } - } - annotatedItem.location?.let { - Row( - modifier = Modifier.padding(start = 8.dp), - horizontalArrangement = Arrangement.spacedBy(space = 4.dp), - ) { - Text( - style = typography.labelSmall, - fontWeight = FontWeight.Bold, - text = stringResource(id = R.string.search_item_location), - ) - Text( - style = typography.labelSmall, - fontStyle = FontStyle.Italic, - text = it, - ) - } - } - annotatedItem.description?.let { - Column( - modifier = Modifier.padding(start = 8.dp) - ) { - Text( - style = typography.labelSmall, - fontWeight = FontWeight.Bold, - text = stringResource(id = R.string.search_item_description), - ) - Text( - style = typography.labelSmall, - fontStyle = FontStyle.Italic, - text = it, - ) - } - } - annotatedItem.history?.let { - Column( - modifier = Modifier.padding(start = 8.dp), - ) { - Text( - style = typography.labelSmall, - fontWeight = FontWeight.Bold, - text = stringResource(id = R.string.search_item_history), - ) - Text( - style = typography.labelSmall, - fontStyle = FontStyle.Italic, - text = it, - ) - } - } - annotatedItem.tags?.let { - Column( - modifier = Modifier.padding(start = 8.dp), - ) { - Text( - style = typography.labelSmall, - fontWeight = FontWeight.Bold, - text = stringResource(id = R.string.search_item_tags), - ) - Text( - style = typography.labelSmall, - fontStyle = FontStyle.Italic, - 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(search = "female"), - SearchItemUio.preview(search = "orc"), - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/search/SearchScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/search/SearchScreen.kt deleted file mode 100644 index a75f317..0000000 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/search/SearchScreen.kt +++ /dev/null @@ -1,231 +0,0 @@ -package com.pixelized.rplexicon.ui.screens.lexicon.search - -import android.content.res.Configuration.UI_MODE_NIGHT_NO -import android.content.res.Configuration.UI_MODE_NIGHT_YES -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.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 -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -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 -import androidx.compose.material3.TopAppBar -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import com.pixelized.rplexicon.NO_WINDOW_INSETS -import com.pixelized.rplexicon.R -import com.pixelized.rplexicon.ui.composable.CollapsingHeader -import com.pixelized.rplexicon.ui.composable.form.TextField -import com.pixelized.rplexicon.ui.composable.form.TextFieldUio -import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost -import com.pixelized.rplexicon.ui.navigation.screens.navigateToLexiconDetail -import com.pixelized.rplexicon.ui.theme.LexiconTheme -import com.pixelized.rplexicon.utilitary.extentions.lexicon - -@Stable -data class SearchFormUio( - val search: TextFieldUio, -) - -@Composable -fun SearchScreen( - viewModel: SearchViewModel = hiltViewModel(), -) { - val screen = LocalScreenNavHost.current - val lazyState = rememberLazyListState() - - Surface { - SearchScreenContent( - modifier = Modifier - .fillMaxSize() - .systemBarsPadding() - .imePadding(), - lazyColumnState = lazyState, - items = viewModel.filter, - form = viewModel.form, - onItem = { item -> - val form = viewModel.form - screen.navigateToLexiconDetail( - id = item.id, - highlight = form.search.value.value.takeIf { it.isNotEmpty() }, - ) - }, - onBack = { - screen.popBackStack() - } - ) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun SearchScreenContent( - modifier: Modifier = Modifier, - lazyColumnState: LazyListState = rememberLazyListState(), - items: State>, - form: SearchFormUio, - onBack: () -> Unit, - onItem: (SearchItemUio) -> Unit, -) { - Scaffold( - modifier = modifier, - contentWindowInsets = NO_WINDOW_INSETS, - containerColor = Color.Transparent, - topBar = { - TopAppBar( - windowInsets = NO_WINDOW_INSETS, - navigationIcon = { - IconButton(onClick = onBack) { - Icon( - painter = painterResource(id = R.drawable.ic_arrow_back_ios_new_24), - contentDescription = null - ) - } - }, - title = { - Text(text = stringResource(id = R.string.search_field_title)) - }, - ) - }, - ) { paddingValues -> - CollapsingHeader( - modifier = Modifier.padding(paddingValues = paddingValues), - header = { - SearchBox( - modifier = Modifier.shadow(elevation = 4.dp), - form = form, - ) - }, - content = { - LazyColumn( - modifier = Modifier.fillMaxSize(), - state = lazyColumnState, - contentPadding = PaddingValues(vertical = 8.dp), - ) { - items( - items = items.value, - key = { it.id }, - contentType = { "Search" }, - ) { - SearchItem( - modifier = Modifier - .clickable { onItem(it) } - .heightIn(min = MaterialTheme.lexicon.dimens.item), - item = it, - ) - } - } - }, - ) - } -} - -@Composable -private fun SearchBox( - modifier: Modifier = Modifier, - form: SearchFormUio, -) { - Surface( - modifier = modifier, - ) { - Column( - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { - TextField( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - field = form.search, - ) - Divider( - modifier = Modifier.padding(top = 16.dp), - color = MaterialTheme.lexicon.colorScheme.placeholder, - ) - } - } -} - -@Composable -@Preview(uiMode = UI_MODE_NIGHT_NO) -@Preview(uiMode = UI_MODE_NIGHT_YES) -private fun SearchScreenContentPreview() { - LexiconTheme { - Surface { - SearchScreenContent( - modifier = Modifier.fillMaxSize(), - form = SearchFormUio( - search = TextFieldUio.preview(R.string.search_field_search), - ), - items = remember { - mutableStateOf( - listOf( - SearchItemUio.preview( - id = "Brulkhai-1", - name = "Brulkhai", - diminutive = "Bru", - gender = "Female", - race = "Half-orc", - ), - SearchItemUio.preview( - id = "Léandre-1", - name = "Léandre", - diminutive = null, - gender = "Male", - race = "Human", - ), - SearchItemUio.preview( - id = "Nélia-1", - name = "Nélia", - diminutive = null, - gender = "Female", - race = "Elf", - ), - SearchItemUio.preview( - id = "Tigrane-1", - name = "Tigrane", - diminutive = null, - gender = "Male", - race = "Tiefling", - ), - SearchItemUio.preview( - id = "Unathana-1", - name = "Unathana", - diminutive = "Una", - gender = "Female", - race = "Half-elf", - ), - ) - ) - }, - onItem = { }, - onBack = { }, - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/search/SearchViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/search/SearchViewModel.kt deleted file mode 100644 index 3151c92..0000000 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/search/SearchViewModel.kt +++ /dev/null @@ -1,69 +0,0 @@ -package com.pixelized.rplexicon.ui.screens.lexicon.search - -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.lifecycle.ViewModel -import com.pixelized.rplexicon.R -import com.pixelized.rplexicon.data.model.Lexicon -import com.pixelized.rplexicon.data.repository.lexicon.LexiconRepository -import com.pixelized.rplexicon.ui.composable.form.TextFieldUio -import com.pixelized.rplexicon.utilitary.extentions.searchCriterion -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject - -@HiltViewModel -class SearchViewModel @Inject constructor( - repository: LexiconRepository, -) : ViewModel() { - private val _search = mutableStateOf("") - - val form = SearchFormUio( - search = TextFieldUio( - label = R.string.search_field_search, - value = _search, - onValueChange = { - _search.value = it - } - ), - ) - - private var data: List = repository.data.value - - val filter = derivedStateOf { - data.filter { item -> - val search = _search.value.searchCriterion().map { criteria -> - val name = item.name.contains(criteria, true) - val gender = item.gender?.contains(criteria, true) == true - val race = item.race?.contains(criteria, true) == true - val diminutive = item.diminutive?.contains(criteria, true) == true - val status = item.status?.contains(criteria, true) == true - val location = item.location?.contains(criteria, true) == true - val description = item.description?.contains(criteria, true) == true - val history = item.history?.contains(criteria, true) == true - val tag = item.tags?.contains(criteria, true) == true - name || gender || race || diminutive || status || location || description || history || tag - } - search.all { it } - }.map { - it.toSearchUio() - }.sortedBy { - it.name - } - } - - private fun Lexicon.toSearchUio( - search: String = _search.value, - ) = SearchItemUio( - id = this.id, - name = this.name, - diminutive = diminutive?.takeIf { it.isNotBlank() }?.let { "./ $it" }, - gender = this.gender, - race = this.race, - status = this.status, - location = this.location, - description = this.description, - history = this.history, - search = search, - tags = tags, - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetail.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetailScreen.kt similarity index 94% rename from app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetail.kt rename to app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetailScreen.kt index 71491b1..0204947 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetail.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetailScreen.kt @@ -70,9 +70,13 @@ import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost import com.pixelized.rplexicon.ui.navigation.screens.navigateToLocationDetail import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.utilitary.LOS_FULL -import com.pixelized.rplexicon.utilitary.LOS_HOLLOW +import com.pixelized.rplexicon.utilitary.annotate import com.pixelized.rplexicon.utilitary.annotateWithDropCap +import com.pixelized.rplexicon.utilitary.dropCapRegex import com.pixelized.rplexicon.utilitary.extentions.lexicon +import com.pixelized.rplexicon.utilitary.extentions.searchCriterion +import com.pixelized.rplexicon.utilitary.highlightRegex +import com.pixelized.rplexicon.utilitary.styleWith import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -86,7 +90,7 @@ data class LocationDetailUio( ) @Composable -fun LocationDetail( +fun LocationDetailScreen( viewModel: LocationDetailViewModel = hiltViewModel() ) { val uriHandler = LocalUriHandler.current @@ -111,6 +115,7 @@ fun LocationDetail( scrollState = scroll, fantasyMapState = viewModel.fantasyMapState, item = viewModel.location, + highlight = viewModel.highlight, selectedIndex = viewModel.selectedMarquee, mapHighlight = mapHighlight, onBack = { @@ -194,6 +199,7 @@ private fun LocationContent( scrollState: ScrollState, fantasyMapState: FantasyMapState, item: State, + highlight: String?, selectedIndex: State, mapHighlight: State, onBack: () -> Unit, @@ -205,6 +211,10 @@ private fun LocationContent( onZoomIn: () -> Unit, onZoomOut: () -> Unit, ) { + val typography = MaterialTheme.lexicon.typography + val highlightRegex = remember(highlight) { highlightRegex(terms = highlight.searchCriterion()) } + val dropCapRegex = remember { dropCapRegex() } + val filledIconButtonColors = IconButtonDefaults.filledIconButtonColors( containerColor = MaterialTheme.colorScheme.surface, contentColor = MaterialTheme.colorScheme.onSurface, @@ -369,9 +379,10 @@ private fun LocationContent( Text( modifier = Modifier.padding(horizontal = 16.dp), style = MaterialTheme.typography.headlineSmall, - text = annotateWithDropCap( + text = annotate( text = item.value?.name ?: "", - style = MaterialTheme.lexicon.typography.headlineSmallDropCap, + dropCapRegex styleWith typography.headlineSmallDropCap, + highlightRegex styleWith typography.detail.highlightStyle, ), ) item.value?.description?.let { @@ -380,9 +391,10 @@ private fun LocationContent( .fillMaxWidth() .padding(horizontal = 16.dp), style = MaterialTheme.typography.bodyMedium, - text = annotateWithDropCap( + text = annotate( text = it, - style = MaterialTheme.lexicon.typography.bodyMediumDropCap + dropCapRegex styleWith typography.bodyMediumDropCap, + highlightRegex styleWith typography.detail.highlightStyle, ), ) } @@ -415,10 +427,11 @@ private fun LocationContent( Text( modifier = Modifier.alignByBaseline(), style = MaterialTheme.typography.bodyMedium, - text = annotateWithDropCap( + text = annotate( text = it.name, - style = MaterialTheme.lexicon.typography.bodyMediumDropCap, - ) + dropCapRegex styleWith typography.bodyMediumDropCap, + highlightRegex styleWith typography.detail.highlightStyle, + ), ) } } @@ -507,6 +520,7 @@ private fun LocationPreview() { ) ) }, + highlight = "ba zaro", selectedIndex = remember { mutableIntStateOf(0) }, mapHighlight = remember { mutableStateOf(Offset(0.5f, 0.5f)) }, onBack = { }, diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetailViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetailViewModel.kt index b739492..24537ed 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetailViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/detail/LocationDetailViewModel.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.geometry.Offset import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.SavedStateHandle import com.pixelized.rplexicon.data.repository.lexicon.LocationRepository +import com.pixelized.rplexicon.ui.navigation.screens.lexiconDetailArgument import com.pixelized.rplexicon.ui.navigation.screens.locationDetailArgument import com.pixelized.rplexicon.utilitary.cells import com.pixelized.rplexicon.utilitary.line @@ -33,6 +34,8 @@ class LocationDetailViewModel @Inject constructor( private val _selectedMarquee = mutableStateOf(null) val selectedMarquee: State get() = _selectedMarquee + val highlight: String? = savedStateHandle.lexiconDetailArgument.highlight + val fantasyMapState = FantasyMapState( initialScale = 1f, initialOffset = Offset.Zero, diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/list/LocationScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/list/LocationScreen.kt index 04b2930..52d183a 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/list/LocationScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/list/LocationScreen.kt @@ -1,7 +1,9 @@ package com.pixelized.rplexicon.ui.screens.location.list import android.content.res.Configuration -import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -14,26 +16,36 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.pullrefresh.PullRefreshState import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope 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.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import com.pixelized.rplexicon.R import com.pixelized.rplexicon.ui.composable.CategoryHeader +import com.pixelized.rplexicon.ui.composable.FloatingActionButton import com.pixelized.rplexicon.ui.composable.Loader import com.pixelized.rplexicon.ui.composable.error.HandleFetchError import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost +import com.pixelized.rplexicon.ui.navigation.screens.navigateToSearch import com.pixelized.rplexicon.ui.navigation.screens.navigateToLocationDetail import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.utilitary.extentions.cell import com.pixelized.rplexicon.utilitary.extentions.lexicon +import com.pixelized.rplexicon.utilitary.extentions.scroll import kotlinx.coroutines.launch @@ -54,6 +66,11 @@ fun LocationScreen( } }, ) + val isFabExpended = remember { + derivedStateOf { + viewModel.items.value.isNotEmpty() && lazyListState.scroll && lazyListState.canScrollForward.not() + } + } Surface { LocationContent( @@ -61,6 +78,10 @@ fun LocationScreen( lazyColumnState = lazyListState, refreshState = refresh, refreshing = viewModel.isLoading, + isFabExpended = isFabExpended, + onSearch = { + screen.navigateToSearch(enableLocations = true) + }, onItem = { screen.navigateToLocationDetail(id = it.id) }, @@ -80,70 +101,75 @@ private fun LocationContent( refreshState: PullRefreshState, refreshing: State, items: State>, + isFabExpended: State, + onSearch: () -> Unit, onItem: (LocationItemUio) -> Unit, ) { Box( modifier = modifier, contentAlignment = Alignment.TopCenter, ) { - AnimatedContent( - targetState = items.value.isEmpty(), - transitionSpec = MaterialTheme.lexicon.animation.itemList, - label = "AnimatedLocations" - ) { empty -> - when (empty) { - true -> LazyColumn( - modifier = Modifier - .fillMaxSize() - .pullRefresh(state = refreshState), - state = lazyColumnState, - contentPadding = MaterialTheme.lexicon.dimens.itemListPadding, - ) { - items(count = 2) { - LocationItem( - modifier = Modifier.cell(), - item = LocationItemUio.preview(placeHolder = true), + LazyColumn( + modifier = Modifier + .fillMaxSize() + .pullRefresh(state = refreshState), + state = lazyColumnState, + contentPadding = MaterialTheme.lexicon.dimens.itemListPadding, + ) { + items.value.forEachIndexed { index, entry -> + entry.category?.let { + item( + contentType = { "Header" }, + ) { + CategoryHeader( + modifier = Modifier + .padding(top = if (index == 0) 0.dp else 16.dp) + .padding(horizontal = 16.dp), + text = it, ) } } - - else -> LazyColumn( - modifier = Modifier - .fillMaxSize() - .pullRefresh(state = refreshState), - state = lazyColumnState, - contentPadding = MaterialTheme.lexicon.dimens.itemListPadding, + items( + items = entry.maps, + key = { it.id }, + contentType = { "Location" }, ) { - items.value.forEachIndexed { index, entry -> - entry.category?.let { - item( - contentType = { "Header" }, - ) { - CategoryHeader( - modifier = Modifier - .padding(top = if (index == 0) 0.dp else 16.dp) - .padding(horizontal = 16.dp), - text = it, - ) - } - } - items( - items = entry.maps, - key = { it.id }, - contentType = { "Location" }, - ) { - LocationItem( - modifier = Modifier - .clickable { onItem(it) } - .cell(), - item = it, - ) - } - } + LocationItem( + modifier = Modifier + .clickable { onItem(it) } + .cell(), + item = it, + ) } } } + AnimatedVisibility( + modifier = Modifier + .padding(all = 16.dp) + .align(Alignment.BottomEnd), + visible = items.value.isNotEmpty(), + enter = fadeIn(), + exit = fadeOut(), + ) { + FloatingActionButton( + expended = isFabExpended.value, + onClick = onSearch, + colors = ButtonDefaults.outlinedButtonColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + icon = { + Icon( + painter = painterResource(id = R.drawable.ic_baseline_search_24), + contentDescription = null, + ) + }, + text = { + Text(text = stringResource(id = R.string.lexicon_search)) + }, + ) + } + Loader( refreshState = refreshState, refreshing = refreshing, @@ -179,6 +205,8 @@ private fun QuestListPreview() { ), ) }, + isFabExpended = remember { mutableStateOf(true) }, + onSearch = { }, onItem = { }, ) } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/detail/QuestDetailScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/detail/QuestDetailScreen.kt index 74ad2dd..352d0e0 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/detail/QuestDetailScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/detail/QuestDetailScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState @@ -29,12 +30,15 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -43,6 +47,7 @@ import androidx.compose.ui.text.style.TextOverflow 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 androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.pixelized.rplexicon.R @@ -56,9 +61,13 @@ import com.pixelized.rplexicon.ui.navigation.screens.navigateToLocationDetail import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.utilitary.LOS_FULL import com.pixelized.rplexicon.utilitary.LOS_HOLLOW -import com.pixelized.rplexicon.utilitary.annotateWithDropCap +import com.pixelized.rplexicon.utilitary.annotate +import com.pixelized.rplexicon.utilitary.dropCapRegex import com.pixelized.rplexicon.utilitary.extentions.lexicon import com.pixelized.rplexicon.utilitary.extentions.scrollOffset +import com.pixelized.rplexicon.utilitary.extentions.searchCriterion +import com.pixelized.rplexicon.utilitary.highlightRegex +import com.pixelized.rplexicon.utilitary.styleWith @Stable data class QuestDetailUio( @@ -93,6 +102,7 @@ fun QuestDetailScreen( QuestDetailContent( modifier = Modifier.fillMaxSize(), item = viewModel.quest, + highlight = viewModel.highlight, onBack = { screen.popBackStack() }, onGiver = { screen.navigateToLexiconDetail(id = it) }, onLocation = { screen.navigateToLocationDetail(id = it) }, @@ -111,11 +121,15 @@ private fun QuestDetailContent( modifier: Modifier = Modifier, state: ScrollState = rememberScrollState(), item: State, + highlight: String?, onBack: () -> Unit, onGiver: (String) -> Unit, onLocation: (String) -> Unit, onImage: (Uri) -> Unit, ) { + val typography = MaterialTheme.lexicon.typography + val highlightRegex = remember(highlight) { highlightRegex(terms = highlight.searchCriterion()) } + val dropCapRegex = remember { dropCapRegex() } val quest = item.value Scaffold( @@ -182,10 +196,11 @@ private fun QuestDetailContent( .padding(horizontal = 16.dp) .padding(bottom = 16.dp), textAlign = TextAlign.Center, - style = MaterialTheme.typography.headlineLarge, - text = annotateWithDropCap( + style = typography.base.headlineLarge, + text = annotate( text = quest?.title ?: "", - style = MaterialTheme.lexicon.typography.headlineLargeDropCap, + dropCapRegex styleWith typography.headlineLargeDropCap, + highlightRegex styleWith typography.detail.highlightStyle, ), ) @@ -193,7 +208,7 @@ private fun QuestDetailContent( Column( verticalArrangement = Arrangement.spacedBy(8.dp), ) { - quest.subtitle?.let { subtitle -> + quest.subtitle?.let { Text( modifier = Modifier .padding(horizontal = 16.dp) @@ -202,9 +217,10 @@ private fun QuestDetailContent( textAlign = TextAlign.Center, overflow = TextOverflow.Ellipsis, maxLines = 3, - text = annotateWithDropCap( - text = subtitle, - style = MaterialTheme.lexicon.typography.titleLargeDropCap, + text = annotate( + text = it, + dropCapRegex styleWith typography.titleLargeDropCap, + highlightRegex styleWith typography.detail.highlightStyle, ), ) } @@ -226,104 +242,131 @@ private fun QuestDetailContent( ) Text( style = MaterialTheme.typography.bodyMedium, - text = when (quest.giverId) { - null -> "$LOS_HOLLOW $it" - else -> "$LOS_FULL $it" - }, + text = annotate( + text = when (quest.giverId) { + null -> "$LOS_HOLLOW $it" + else -> "$LOS_FULL $it" + }, + highlightRegex styleWith typography.detail.highlightStyle, + ) ) } } + } - quest.place?.let { - Column( - modifier = Modifier - .fillMaxWidth() - .clickable( - enabled = quest.placeId != null, - onClick = { quest.placeId?.let { onLocation(it) } } - ) - .padding(horizontal = 16.dp), - ) { - Text( - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - text = stringResource(id = R.string.quest_detail_area), + quest.place?.let { + Column( + modifier = Modifier + .fillMaxWidth() + .clickable( + enabled = quest.placeId != null, + onClick = { quest.placeId?.let { onLocation(it) } } ) - Text( - style = MaterialTheme.typography.bodyMedium, + .padding(horizontal = 16.dp), + ) { + Text( + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + text = stringResource(id = R.string.quest_detail_area), + ) + Text( + style = MaterialTheme.typography.bodyMedium, + text = annotate( text = when (quest.placeId) { null -> "$LOS_HOLLOW $it" else -> "$LOS_FULL $it" }, - ) - } + highlightRegex styleWith typography.detail.highlightStyle + ), + ) } + } - quest.globalReward?.let { - Column( - modifier = Modifier.padding(horizontal = 16.dp), - ) { - Text( - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - text = stringResource(id = R.string.quest_detail_group_reward), - ) - Text( - style = MaterialTheme.typography.bodyMedium, - text = "$LOS_HOLLOW $it", - ) - } - } - - quest.individualReward?.let { - Column( - modifier = Modifier.padding(horizontal = 16.dp), - ) { - Text( - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.Bold, - text = stringResource(id = R.string.quest_detail_individual_reward), - ) - Text( - style = MaterialTheme.typography.bodyMedium, - text = "$LOS_HOLLOW $it", - ) - } - } - - Text( + quest.globalReward?.let { + Column( modifier = Modifier.padding(horizontal = 16.dp), - style = MaterialTheme.typography.bodyMedium, - text = annotateWithDropCap( - text = quest.description, - style = MaterialTheme.lexicon.typography.bodyMediumDropCap, - ), - ) + ) { + Text( + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + text = stringResource(id = R.string.quest_detail_group_reward), + ) + Text( + style = MaterialTheme.typography.bodyMedium, + text = annotate( + text = "$LOS_HOLLOW $it", + highlightRegex styleWith typography.detail.highlightStyle, + ), + ) + } + } - if (quest.images.isNotEmpty()) { - LazyRow( - contentPadding = PaddingValues(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - items(items = quest.images) { - AsyncImage( - modifier = Modifier - .clickable { onImage(it) } - .height(height = 160.dp), - contentScale = ContentScale.FillHeight, - model = it, - ) - } + quest.individualReward?.let { + Column( + modifier = Modifier.padding(horizontal = 16.dp), + ) { + Text( + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + text = stringResource(id = R.string.quest_detail_individual_reward), + ) + Text( + style = MaterialTheme.typography.bodyMedium, + text = annotate( + text = "$LOS_HOLLOW $it", + highlightRegex styleWith typography.detail.highlightStyle, + ), + ) + } + } + + Text( + modifier = Modifier.padding(horizontal = 16.dp), + style = MaterialTheme.typography.bodyMedium, + text = annotate( + text = quest.description, + dropCapRegex styleWith typography.bodyMediumDropCap, + highlightRegex styleWith typography.detail.highlightStyle, + ), + ) + + if (quest.images.isNotEmpty()) { + LazyRow( + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(items = quest.images) { + AsyncImage( + modifier = Modifier + .clickable { onImage(it) } + .sizeIn(maxWidth = rememberMaxWidth()) + .height(height = 160.dp), + contentScale = ContentScale.FillHeight, + model = it, + ) } } } } } } - }, + } ) } +@Composable +private fun rememberMaxWidth(): Dp { + val configuration = LocalConfiguration.current + val view = LocalView.current + return remember(configuration, view) { + if (view.isInEditMode) { + 300.dp + } else { + (configuration.screenWidthDp.dp - 16.dp * 2) + } + } +} + @Composable @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @@ -334,6 +377,7 @@ private fun QuestDetailPreview( Surface { QuestDetailContent( item = preview, + highlight = "chasse", onBack = { }, onGiver = { }, onLocation = { }, diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/detail/QuestDetailViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/detail/QuestDetailViewModel.kt index 1e266ac..1ed2e31 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/detail/QuestDetailViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/detail/QuestDetailViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModel import com.pixelized.rplexicon.data.repository.lexicon.LexiconRepository import com.pixelized.rplexicon.data.repository.lexicon.LocationRepository import com.pixelized.rplexicon.data.repository.lexicon.QuestRepository +import com.pixelized.rplexicon.ui.navigation.screens.lexiconDetailArgument import com.pixelized.rplexicon.ui.navigation.screens.questDetailArgument import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -21,6 +22,8 @@ class QuestDetailViewModel @Inject constructor( private val _quest = mutableStateOf(null) val quest: State get() = _quest + val highlight: String? = savedStateHandle.lexiconDetailArgument.highlight + init { val argument = savedStateHandle.questDetailArgument val source = questRepository.find(id = argument.id) diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/list/QuestListScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/list/QuestListScreen.kt index ea5bce2..18317ef 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/list/QuestListScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/list/QuestListScreen.kt @@ -1,7 +1,9 @@ package com.pixelized.rplexicon.ui.screens.quest.list import android.content.res.Configuration -import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -14,26 +16,36 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.pullrefresh.PullRefreshState import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope 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.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import com.pixelized.rplexicon.R import com.pixelized.rplexicon.ui.composable.CategoryHeader +import com.pixelized.rplexicon.ui.composable.FloatingActionButton import com.pixelized.rplexicon.ui.composable.Loader import com.pixelized.rplexicon.ui.composable.error.HandleFetchError import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost import com.pixelized.rplexicon.ui.navigation.screens.navigateToQuestDetail +import com.pixelized.rplexicon.ui.navigation.screens.navigateToSearch import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.utilitary.extentions.cell import com.pixelized.rplexicon.utilitary.extentions.lexicon +import com.pixelized.rplexicon.utilitary.extentions.scroll import kotlinx.coroutines.launch @@ -54,6 +66,11 @@ fun QuestListScreen( } }, ) + val isFabExpended = remember { + derivedStateOf { + viewModel.items.value.isNotEmpty() && lazyListState.scroll && lazyListState.canScrollForward.not() + } + } Surface { QuestListContent( @@ -61,6 +78,10 @@ fun QuestListScreen( lazyColumnState = lazyListState, refreshState = refresh, refreshing = viewModel.isLoading, + isFabExpended = isFabExpended, + onSearch = { + screen.navigateToSearch(enableQuests = true) + }, onItem = { screen.navigateToQuestDetail(id = it.id) }, @@ -79,6 +100,8 @@ private fun QuestListContent( lazyColumnState: LazyListState, refreshState: PullRefreshState, refreshing: State, + isFabExpended: State, + onSearch: () -> Unit, items: State>, onItem: (QuestItemUio) -> Unit, ) { @@ -86,62 +109,65 @@ private fun QuestListContent( modifier = modifier, contentAlignment = Alignment.TopCenter, ) { - AnimatedContent( - targetState = items.value.isEmpty(), - transitionSpec = MaterialTheme.lexicon.animation.itemList, - label = "AnimatedQuests" - ) { empty -> - when (empty) { - true -> LazyColumn( - modifier = Modifier - .fillMaxSize() - .pullRefresh(state = refreshState), - state = lazyColumnState, - contentPadding = MaterialTheme.lexicon.dimens.itemListPadding, + LazyColumn( + modifier = Modifier + .fillMaxSize() + .pullRefresh(state = refreshState), + state = lazyColumnState, + contentPadding = MaterialTheme.lexicon.dimens.itemListPadding, + ) { + items.value.forEachIndexed { index, entry -> + item( + contentType = { "Header" }, ) { - items(count = 4) { - QuestItem( - modifier = Modifier.cell(), - item = QuestItemUio.preview(placeHolder = true), - ) - } + CategoryHeader( + modifier = Modifier + .padding(top = if (index == 0) 0.dp else 16.dp) + .padding(horizontal = 16.dp), + text = entry.category, + ) } - - else -> LazyColumn( - modifier = Modifier - .fillMaxSize() - .pullRefresh(state = refreshState), - state = lazyColumnState, - contentPadding = MaterialTheme.lexicon.dimens.itemListPadding, + items( + items = entry.quests, + key = { it.id }, + contentType = { "Quest" }, ) { - items.value.forEachIndexed { index, entry -> - item( - contentType = { "Header" }, - ) { - CategoryHeader( - modifier = Modifier - .padding(top = if (index == 0) 0.dp else 16.dp) - .padding(horizontal = 16.dp), - text = entry.category, - ) - } - items( - items = entry.quests, - key = { it.id }, - contentType = { "Quest" }, - ) { - QuestItem( - modifier = Modifier - .clickable { onItem(it) } - .cell(), - item = it, - ) - } - } + QuestItem( + modifier = Modifier + .clickable { onItem(it) } + .cell(), + item = it, + ) } } } + AnimatedVisibility( + modifier = Modifier + .padding(all = 16.dp) + .align(Alignment.BottomEnd), + visible = items.value.isNotEmpty(), + enter = fadeIn(), + exit = fadeOut(), + ) { + FloatingActionButton( + expended = isFabExpended.value, + onClick = onSearch, + colors = ButtonDefaults.outlinedButtonColors( + containerColor = MaterialTheme.colorScheme.surface, + ), + icon = { + Icon( + painter = painterResource(id = R.drawable.ic_baseline_search_24), + contentDescription = null, + ) + }, + text = { + Text(text = stringResource(id = R.string.lexicon_search)) + }, + ) + } + Loader( refreshState = refreshState, refreshing = refreshing, @@ -183,6 +209,8 @@ private fun QuestListPreview() { ) ) }, + isFabExpended = remember { mutableStateOf(true) }, + onSearch = { }, onItem = { }, ) } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/ThrowsCard.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/ThrowsCard.kt index f83a394..9cf1045 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/ThrowsCard.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/rolls/composable/ThrowsCard.kt @@ -49,7 +49,7 @@ import com.pixelized.rplexicon.R import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.utilitary.extentions.annotatedSpan import com.pixelized.rplexicon.utilitary.extentions.ddBorder -import com.pixelized.rplexicon.utilitary.extentions.highlightRegex +import com.pixelized.rplexicon.utilitary.highlightRegex import java.util.UUID @Stable @@ -116,15 +116,16 @@ fun ThrowsCard( .animateContentSize(), ) { throws.title?.let { + val highlightRegex = remember(it) { highlightRegex(term = it) } Text( color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.65f), style = MaterialTheme.typography.bodyLarge, text = AnnotatedString( text = it, - spanStyles = throws.highlight?.highlightRegex?.annotatedSpan( + spanStyles = highlightRegex.annotatedSpan( input = it, - spanStyle = highlight, - ) ?: emptyList() + style = highlight, + ) ), ) } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchFilter.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchFilter.kt new file mode 100644 index 0000000..84a9863 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchFilter.kt @@ -0,0 +1,110 @@ +package com.pixelized.rplexicon.ui.screens.search + +import android.content.res.Configuration +import androidx.annotation.StringRes +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +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.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +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.ui.theme.LexiconTheme +import com.pixelized.rplexicon.utilitary.extentions.lexicon + +@Stable +sealed class SearchFilterUio( + @StringRes val label: Int, + val selected: State, +) { + class Lexicon( + selected: State = mutableStateOf(true), + ) : SearchFilterUio( + label = R.string.home_lexicon, + selected = selected, + ) + + class Quest( + selected: State = mutableStateOf(true), + ) : SearchFilterUio( + label = R.string.home_quest_log, + selected = selected, + ) + + class Location( + selected: State = mutableStateOf(true), + ) : SearchFilterUio( + label = R.string.home_location, + selected = selected, + ) + + class Spell( + selected: State = mutableStateOf(true), + ) : SearchFilterUio( + label = R.string.spell_detail_title, + selected = selected, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SearchFilter( + modifier: Modifier = Modifier, + filter: SearchFilterUio, + onFilter: (SearchFilterUio) -> Unit, +) { + FilterChip( + modifier = modifier, + shape = CircleShape, + colors = FilterChipDefaults.filterChipColors( + containerColor = MaterialTheme.lexicon.colorScheme.base.surface, + selectedContainerColor = MaterialTheme.lexicon.colorScheme.base.surface, + labelColor = MaterialTheme.lexicon.colorScheme.search.chip.unSelected, + selectedLabelColor = MaterialTheme.lexicon.colorScheme.search.chip.selected, + ), + border = FilterChipDefaults.filterChipBorder( + borderColor = MaterialTheme.lexicon.colorScheme.search.chip.unSelected, + selectedBorderColor = MaterialTheme.lexicon.colorScheme.search.chip.selected, + selectedBorderWidth = 1.dp, + ), + selected = filter.selected.value, + onClick = { onFilter(filter) }, + label = { Text(text = stringResource(id = filter.label)) } + ) +} + +@Composable +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun SearchFilterPreview( + @PreviewParameter(SearchFilterPreviewProvider::class) preview: Boolean +) { + LexiconTheme { + Surface { + SearchFilter( + filter = remember { + SearchFilterUio.Lexicon( + selected = mutableStateOf(preview), + ) + }, + onFilter = { }, + ) + } + } +} + +private class SearchFilterPreviewProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf(true, false) +} \ 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 new file mode 100644 index 0000000..3fa910c --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchScreen.kt @@ -0,0 +1,238 @@ +package com.pixelized.rplexicon.ui.screens.search + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement +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.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.pixelized.rplexicon.NO_WINDOW_INSETS +import com.pixelized.rplexicon.R +import com.pixelized.rplexicon.ui.composable.form.TextFieldAppBar +import com.pixelized.rplexicon.ui.composable.form.TextFieldUio +import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost +import com.pixelized.rplexicon.ui.navigation.screens.navigateToLexiconDetail +import com.pixelized.rplexicon.ui.navigation.screens.navigateToLocationDetail +import com.pixelized.rplexicon.ui.navigation.screens.navigateToQuestDetail +import com.pixelized.rplexicon.ui.screens.search.item.LexiconSearchItem +import com.pixelized.rplexicon.ui.screens.search.item.LocationSearchItem +import com.pixelized.rplexicon.ui.screens.search.item.QuestSearchItem +import com.pixelized.rplexicon.ui.screens.search.item.SearchItemUio +import com.pixelized.rplexicon.ui.screens.search.item.SearchItemUio.LexiconSearchItemUio +import com.pixelized.rplexicon.ui.screens.search.item.SearchItemUio.LocationSearchItemUio +import com.pixelized.rplexicon.ui.screens.search.item.SearchItemUio.QuestSearchItemUio +import com.pixelized.rplexicon.ui.theme.LexiconTheme +import com.pixelized.rplexicon.utilitary.PUC_FULL +import com.pixelized.rplexicon.utilitary.extentions.lexiconShadow + +@Stable +data class SearchFormUio( + val search: TextFieldUio, + val filter: List, +) { + val highlight get() = search.value.value +} + +@Composable +fun SearchScreen( + searchViewModel: SearchViewModel = hiltViewModel(), +) { + val screen = LocalScreenNavHost.current + val form = searchViewModel.rememberSearchForm() + + Surface { + SearchScreenContent( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding() + .imePadding(), + form = form, + data = searchViewModel.data, + onBack = { screen.popBackStack() }, + onClear = searchViewModel::clear, + onChip = searchViewModel::onChip, + onSearchChange = searchViewModel::onSearchChange, + onLexicon = { + screen.navigateToLexiconDetail(id = it, highlight = form.highlight) + }, + onQuest = { + screen.navigateToQuestDetail(id = it, highlight = form.highlight) + }, + onLocation = { + screen.navigateToLocationDetail(id = it, highlight = form.highlight) + }, + ) + } +} + +@Composable +private fun SearchScreenContent( + modifier: Modifier = Modifier, + form: SearchFormUio, + data: State>, + onBack: () -> Unit, + onClear: () -> Unit, + onChip: (SearchFilterUio) -> Unit, + onSearchChange: (String) -> Unit, + onLexicon: (String) -> Unit, + onQuest: (String) -> Unit, + onLocation: (String) -> Unit, +) { + Scaffold( + modifier = modifier, + contentWindowInsets = NO_WINDOW_INSETS, + topBar = { + Surface( + modifier = Modifier.lexiconShadow(), + ) { + TextFieldAppBar( + modifier = Modifier + .fillMaxWidth() + .height(height = 64.dp), + field = form.search, + onBack = onBack, + onClear = onClear, + onValueChange = onSearchChange, + ) + } + }, + content = { paddingValues -> + Box( + modifier = Modifier.padding(paddingValues) + ) { + LazyColumn( + contentPadding = PaddingValues(top = 8.dp), + content = { + if (form.filter.isNotEmpty()) { + item { + LazyRow( + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(space = 8.dp), + content = { + items(items = form.filter) { + SearchFilter( + filter = it, + onFilter = onChip, + ) + } + } + ) + } + } + items( + items = data.value, + key = { + when (it) { + is LexiconSearchItemUio -> "$TYPE_LEXICON-${it.id}" + is LocationSearchItemUio -> "$TYPE_LOCATION-${it.id}" + is QuestSearchItemUio -> "$TYPE_QUEST-${it.id}" + } + }, + contentType = { + when (it) { + is LexiconSearchItemUio -> TYPE_LEXICON + is LocationSearchItemUio -> TYPE_LOCATION + is QuestSearchItemUio -> TYPE_QUEST + } + } + ) { item -> + when (item) { + is LocationSearchItemUio -> LocationSearchItem( + item = item, + onLocation = onLocation, + ) + + is LexiconSearchItemUio -> LexiconSearchItem( + item = item, + onLexicon = onLexicon, + ) + + is QuestSearchItemUio -> QuestSearchItem( + item = item, + onQuest = onQuest, + ) + } + } + } + ) + } + } + ) +} + +@Composable +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +fun SearchScreenPreview() { + LexiconTheme { + SearchScreenContent( + modifier = Modifier + .systemBarsPadding() + .imePadding(), + form = remember { + SearchFormUio( + search = TextFieldUio.preview( + label = R.string.search_field_title, + value = "a", + ), + filter = listOf( + SearchFilterUio.Lexicon(), + SearchFilterUio.Quest(), + SearchFilterUio.Location(), + SearchFilterUio.Spell(selected = mutableStateOf(false)), + ) + ) + }, + data = remember { + mutableStateOf( + listOf( + LocationSearchItemUio( + id = "", + category = AnnotatedString(text = "Cartes $PUC_FULL Région de Vallaki"), + title = AnnotatedString(text = "Vallaki"), + description = null, + destination = null, + ), + LocationSearchItemUio( + id = "", + category = AnnotatedString(text = "Cartes $PUC_FULL Contré de la brume"), + title = AnnotatedString(text = "Barovie"), + description = AnnotatedString(text = "Contré sombre et maudite somise au joug de Stradh von Zarovich"), + destination = AnnotatedString(text = "Barovie (village)"), + ), + ) + ) + }, + onBack = { }, + onClear = { }, + onSearchChange = { }, + onChip = { }, + onLexicon = { }, + onQuest = { }, + onLocation = { }, + ) + } +} + +private const val TYPE_LEXICON = "Lexicon" +private const val TYPE_QUEST = "Quest" +private const val TYPE_LOCATION = "Location" \ No newline at end of file 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 new file mode 100644 index 0000000..d44589d --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchViewModel.kt @@ -0,0 +1,177 @@ +package com.pixelized.rplexicon.ui.screens.search + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.pixelized.rplexicon.R +import com.pixelized.rplexicon.business.SearchUseCase +import com.pixelized.rplexicon.data.model.Lexicon +import com.pixelized.rplexicon.data.model.Location +import com.pixelized.rplexicon.data.model.Quest +import com.pixelized.rplexicon.data.repository.lexicon.LexiconRepository +import com.pixelized.rplexicon.data.repository.lexicon.LocationRepository +import com.pixelized.rplexicon.data.repository.lexicon.QuestRepository +import com.pixelized.rplexicon.ui.composable.form.TextFieldUio +import com.pixelized.rplexicon.ui.navigation.screens.searchArgument +import com.pixelized.rplexicon.ui.screens.search.item.SearchItemUio +import com.pixelized.rplexicon.utilitary.extentions.lexicon +import com.pixelized.rplexicon.utilitary.extentions.searchCriterion +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +@HiltViewModel +class SearchViewModel @Inject constructor( + private val lexiconRepository: LexiconRepository, + private val questRepository: QuestRepository, + private val locationRepository: LocationRepository, + private val searchUseCase: SearchUseCase, + savedStateHandle: SavedStateHandle, +) : ViewModel() { + private var searchJob: Job? = null + private val enableLexicon = MutableStateFlow(savedStateHandle.searchArgument.enableLexicon) + private val enableQuests = MutableStateFlow(savedStateHandle.searchArgument.enableQuests) + private val enableLocations = MutableStateFlow(savedStateHandle.searchArgument.enableLocations) + private val search = MutableStateFlow("") + + private val _data = mutableStateOf>(emptyList()) + val data: State> get() = _data + + @Composable + @Stable + fun rememberSearchForm(): SearchFormUio { + val context = LocalContext.current + val typography = MaterialTheme.lexicon.typography + + LaunchedEffect(key1 = "SearchViewModel-Search") { + launch(Dispatchers.IO) { + val scope = SearchScope() + lexiconRepository.data + .combine(questRepository.data) { lexicon, quests -> + scope.lexicon = lexicon + scope.quests = quests + } + .combine(locationRepository.data) { _, locations -> + scope.locations = locations + } + .combine(enableLexicon) { _, enable -> + scope.isLexiconEnable = enable + } + .combine(enableQuests) { _, enable -> + scope.isQuestsEnable = enable + } + .combine(enableLocations) { _, enable -> + scope.isLocationsEnable = enable + } + .combine(search) { _, search -> + scope.search = search.searchCriterion() + } + .collect { + if (scope.search.any()) { + val lexicon = if (scope.isLexiconEnable) { + searchUseCase.filterLexicon( + context = context, + typography = typography, + lexicon = scope.lexicon, + criterion = scope.search, + ) + } else { + emptyList() + } + val quests = if (scope.isQuestsEnable) { + searchUseCase.filterQuests( + context = context, + typography = typography, + quests = scope.quests, + criterion = scope.search, + ) + } else { + emptyList() + } + val locations = if (scope.isLocationsEnable) { + searchUseCase.filterLocation( + context = context, + typography = typography, + locations = scope.locations, + criterion = scope.search, + ) + } else { + emptyList() + } + val data = (lexicon + quests + locations).sortedBy { it.sort } + withContext(Dispatchers.Main) { + _data.value = data + } + } else { + withContext(Dispatchers.Main) { + _data.value = emptyList() + } + } + } + } + } + + val searchTextField = search.collectAsState() + val enableLexicon = this.enableLexicon.collectAsState() + val enableQuests = this.enableQuests.collectAsState() + val enableLocations = this.enableLocations.collectAsState() + return remember { + SearchFormUio( + search = TextFieldUio( + label = R.string.search_field_title, + value = searchTextField, + ), + filter = listOf( + SearchFilterUio.Lexicon(selected = enableLexicon), + SearchFilterUio.Quest(selected = enableQuests), + SearchFilterUio.Location(selected = enableLocations), + ) + ) + } + } + + fun onChip(chip: SearchFilterUio) { + searchJob?.cancel() + searchJob = viewModelScope.launch(Dispatchers.IO) { + when (chip) { + is SearchFilterUio.Lexicon -> enableLexicon.emit(chip.selected.value.not()) + is SearchFilterUio.Location -> enableLocations.emit(chip.selected.value.not()) + is SearchFilterUio.Quest -> enableQuests.emit(chip.selected.value.not()) + is SearchFilterUio.Spell -> TODO() + } + } + } + + fun onSearchChange(text: String) { + searchJob?.cancel() + searchJob = viewModelScope.launch(Dispatchers.IO) { + search.emit(text) + } + } + + fun clear() = onSearchChange("") + + private class SearchScope { + var isLexiconEnable: Boolean = false + var lexicon: List = emptyList() + var isQuestsEnable: Boolean = false + var quests: List = emptyList() + var isLocationsEnable: Boolean = false + var locations: List = emptyList() + var search: List = emptyList() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/item/LexiconSearchItem.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/item/LexiconSearchItem.kt new file mode 100644 index 0000000..e947719 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/item/LexiconSearchItem.kt @@ -0,0 +1,191 @@ +package com.pixelized.rplexicon.ui.screens.search.item + +import android.content.res.Configuration +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.pixelized.rplexicon.ui.theme.LexiconTheme +import com.pixelized.rplexicon.utilitary.PUC_FULL +import com.pixelized.rplexicon.utilitary.annotateWithDropCap +import com.pixelized.rplexicon.utilitary.extentions.lexicon + +@Composable +fun LexiconSearchItem( + modifier: Modifier = Modifier, + item: SearchItemUio.LexiconSearchItemUio, + onLexicon: (String) -> Unit, +) { + SearchItemLayout( + modifier = modifier.clickable { onLexicon(item.id) }, + category = item.category, + content = { + Row( + horizontalArrangement = Arrangement.spacedBy(space = 4.dp), + ) { + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.lexicon.typography.search.title, + fontWeight = FontWeight.Bold, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + text = item.name, + ) + + item.diminutive?.let { + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.lexicon.typography.search.extract, + fontStyle = FontStyle.Normal, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + text = it, + ) + } + + item.gender?.let { + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.lexicon.typography.search.extract, + fontStyle = FontStyle.Italic, + fontWeight = FontWeight.Light, + maxLines = 1, + text = it, + ) + } + + item.race?.let { + Text( + modifier = Modifier.alignByBaseline(), + style = MaterialTheme.lexicon.typography.search.extract, + fontStyle = FontStyle.Italic, + fontWeight = FontWeight.Light, + maxLines = 1, + text = it, + ) + } + } + item.status?.let { + Text( + style = MaterialTheme.lexicon.typography.search.extract, + text = it, + ) + } + item.location?.let { + Text( + style = MaterialTheme.lexicon.typography.search.extract, + text = it, + ) + } + item.description?.let { + Text( + style = MaterialTheme.lexicon.typography.search.extract, + text = it, + ) + } + item.story?.let { + Text( + style = MaterialTheme.lexicon.typography.search.extract, + text = it, + ) + } + item.tags?.let { + Text( + style = MaterialTheme.lexicon.typography.search.extract, + text = it, + ) + } + }, + ) +} + +@Composable +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun LexiconSearchItemPreview() { + LexiconTheme { + Surface { + LexiconSearchItem( + item = SearchItemUio.LexiconSearchItemUio( + id = "", + category = AnnotatedString(text = "Lexique $PUC_FULL Personnage joueur"), + name = annotateWithDropCap( + text = "Brulkhai", + style = MaterialTheme.lexicon.typography.titleMediumDropCap, + ), + diminutive = AnnotatedString( + text = "Bru" + ), + gender = AnnotatedString( + text = "Femme" + ), + race = AnnotatedString( + text = "Demi-orc" + ), + status = AnnotatedString( + text = "Status: Vivant", + spanStyles = listOf( + AnnotatedString.Range( + item = MaterialTheme.lexicon.typography.search.extractBold, + start = 0, + end = "Status".length, + ) + ), + ), + location = AnnotatedString( + text = "Location: Barovie", + spanStyles = listOf( + AnnotatedString.Range( + item = MaterialTheme.lexicon.typography.search.extractBold, + start = 0, + end = "Location".length, + ) + ), + ), + description = AnnotatedString( + text = "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).", + spanStyles = listOf( + AnnotatedString.Range( + item = MaterialTheme.lexicon.typography.search.extractBold, + start = 0, + end = "Description".length, + ) + ), + ), + story = AnnotatedString( + text = "History: Une mère-poule avec Una, bien qu'elle essaie de le cacher derrière une façade et des manières brutes.", + spanStyles = listOf( + AnnotatedString.Range( + item = MaterialTheme.lexicon.typography.search.extractBold, + start = 0, + end = "History".length, + ) + ), + ), + tags = AnnotatedString( + text = "Tags: Brulkhai Griffe-Rétracté.", + spanStyles = listOf( + AnnotatedString.Range( + item = MaterialTheme.lexicon.typography.search.extractBold, + start = 0, + end = "Tags".length, + ) + ), + ), + ), + onLexicon = { }, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/item/LocationSearchItem.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/item/LocationSearchItem.kt new file mode 100644 index 0000000..4317ec0 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/item/LocationSearchItem.kt @@ -0,0 +1,70 @@ +package com.pixelized.rplexicon.ui.screens.search.item + +import android.content.res.Configuration +import androidx.compose.foundation.clickable +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import com.pixelized.rplexicon.ui.theme.LexiconTheme +import com.pixelized.rplexicon.utilitary.PUC_FULL +import com.pixelized.rplexicon.utilitary.extentions.lexicon + +@Composable +fun LocationSearchItem( + modifier: Modifier = Modifier, + item: SearchItemUio.LocationSearchItemUio, + onLocation: (String) -> Unit, +) { + SearchItemLayout( + modifier = modifier.clickable { onLocation(item.id) }, + category = item.category, + content = { + Text( + style = MaterialTheme.lexicon.typography.search.title, + fontWeight = FontWeight.Bold, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + text = item.title, + ) + item.description?.let { + Text( + style = MaterialTheme.lexicon.typography.search.extract, + fontWeight = FontWeight.Normal, + text = it, + ) + } + item.destination?.let { + Text( + style = MaterialTheme.lexicon.typography.search.extract, + text = it, + ) + } + }, + ) +} + +@Composable +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun LocationSearchItemPreview() { + LexiconTheme { + Surface { + LocationSearchItem( + item = SearchItemUio.LocationSearchItemUio( + id = "", + category = AnnotatedString(text = "Carte $PUC_FULL Contrée de la brume"), + title = AnnotatedString(text = "Barovie"), + description = AnnotatedString(text = "Contré sombre et maudite soumise au joug de Stradh von Zarovich"), + destination = AnnotatedString(text = "Barovie (village)"), + ), + onLocation = { }, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/item/QuestSearchItem.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/item/QuestSearchItem.kt new file mode 100644 index 0000000..71df3cd --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/item/QuestSearchItem.kt @@ -0,0 +1,90 @@ +package com.pixelized.rplexicon.ui.screens.search.item + +import android.content.res.Configuration +import androidx.compose.foundation.clickable +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import com.pixelized.rplexicon.ui.theme.LexiconTheme +import com.pixelized.rplexicon.utilitary.PUC_FULL +import com.pixelized.rplexicon.utilitary.extentions.lexicon + +@Composable +fun QuestSearchItem( + modifier: Modifier = Modifier, + item: SearchItemUio.QuestSearchItemUio, + onQuest: (String) -> Unit, +) { + SearchItemLayout( + modifier = modifier.clickable { onQuest(item.id) }, + category = item.category, + content = { + Text( + style = MaterialTheme.lexicon.typography.search.title, + fontWeight = FontWeight.Bold, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + text = item.title, + ) + item.owner?.let { + Text( + style = MaterialTheme.lexicon.typography.search.extract, + text = it, + ) + } + item.location?.let { + Text( + style = MaterialTheme.lexicon.typography.search.extract, + text = it, + ) + } + item.individualReward?.let { + Text( + style = MaterialTheme.lexicon.typography.search.extract, + text = it, + ) + } + item.groupReward?.let { + Text( + style = MaterialTheme.lexicon.typography.search.extract, + text = it, + ) + } + item.description?.let { + Text( + style = MaterialTheme.lexicon.typography.search.extract, + text = it, + ) + } + }, + ) +} + +@Composable +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun QuestSearchItemPreview() { + LexiconTheme { + Surface { + QuestSearchItem( + item = SearchItemUio.QuestSearchItemUio( + id = "", + category = AnnotatedString(text = "Quest $PUC_FULL Les cartes de la destinée"), + title = AnnotatedString(text = "La Brume"), + owner = AnnotatedString(text = ""), + location = AnnotatedString(text = ""), + individualReward = AnnotatedString(text = ""), + groupReward = AnnotatedString(text = ""), + description = AnnotatedString(text = ""), + ), + onQuest = { }, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/item/SearchItemLayout.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/item/SearchItemLayout.kt new file mode 100644 index 0000000..973737d --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/item/SearchItemLayout.kt @@ -0,0 +1,142 @@ +package com.pixelized.rplexicon.ui.screens.search.item + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.minimumInteractiveComponentSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.pixelized.rplexicon.utilitary.LOS_HOLLOW +import com.pixelized.rplexicon.utilitary.extentions.lexicon + +@Composable +fun SearchItemLayout( + modifier: Modifier = Modifier, + paddingValues: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp), + category: AnnotatedString?, + content: @Composable ColumnScope.() -> Unit, +) { + val density = LocalDensity.current + val height = remember { mutableIntStateOf(0) } + + Box( + modifier = modifier + .minimumInteractiveComponentSize() + .fillMaxWidth() + .padding(paddingValues = paddingValues), + contentAlignment = Alignment.CenterStart, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Column( + modifier = Modifier.alignByBaseline(), + ) { + Text( + style = MaterialTheme.lexicon.typography.search.title, + fontWeight = FontWeight.Bold, + text = LOS_HOLLOW, + ) + Queue( + modifier = Modifier + .height(height = with(density) { height.intValue.toDp() }) + .padding(bottom = 2.dp), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + } + Column( + modifier = Modifier.alignByBaseline(), + ) { + category?.let { + Text( + style = MaterialTheme.lexicon.typography.search.category, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + text = it, + ) + } + Column( + modifier = Modifier.onSizeChanged { height.intValue = it.height }, + content = content, + ) + } + } + } +} + +@Composable +private fun Queue( + modifier: Modifier = Modifier, + style: TextStyle, + color: Color, +) { + Box( + modifier = modifier + ) { + Box( + modifier = Modifier + .align(alignment = Alignment.TopCenter) + .size(1.dp, 7.dp) + .offset(y = (-7).dp) + .background(color = color), + ) + Box( + modifier = Modifier + .align(alignment = Alignment.BottomCenter) + .width(1.dp) + .fillMaxHeight() + .background(brush = rememberVerticalBrush()) + ) + Text( + modifier = Modifier + .height(0.dp) + .alpha(0f), + style = style, + fontWeight = FontWeight.Bold, + text = LOS_HOLLOW, + ) + } +} + +@Composable +@Stable +private fun rememberVerticalBrush( + colorScheme: ColorScheme = MaterialTheme.colorScheme +): Brush = remember { + Brush.verticalGradient( + colors = listOf( + colorScheme.onSurface, + colorScheme.onSurface, + colorScheme.surface, + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/item/SearchItemUio.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/item/SearchItemUio.kt new file mode 100644 index 0000000..47b1170 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/item/SearchItemUio.kt @@ -0,0 +1,47 @@ +package com.pixelized.rplexicon.ui.screens.search.item + +import androidx.compose.runtime.Stable +import androidx.compose.ui.text.AnnotatedString + +@Stable +sealed class SearchItemUio( + val sort: String, +) { + abstract val category: AnnotatedString? + + @Stable + data class LexiconSearchItemUio( + val id: String, + override val category: AnnotatedString?, + val name: AnnotatedString, + val diminutive: AnnotatedString?, + val gender: AnnotatedString?, + val race: AnnotatedString?, + val status: AnnotatedString?, + val location: AnnotatedString?, + val description: AnnotatedString?, + val story: AnnotatedString?, + val tags: AnnotatedString?, + ) : SearchItemUio(sort = name.text) + + @Stable + data class LocationSearchItemUio( + val id: String, + override val category: AnnotatedString?, + val title: AnnotatedString, + val description: AnnotatedString?, + val destination: AnnotatedString?, + ) : SearchItemUio(sort = title.text) + + @Stable + data class QuestSearchItemUio( + val id: String, + override val category: AnnotatedString?, + val title: AnnotatedString, + val owner: AnnotatedString?, + val location: AnnotatedString?, + val individualReward: AnnotatedString?, + val groupReward: AnnotatedString?, + val description: AnnotatedString?, + ) : SearchItemUio(sort = title.text) +} \ No newline at end of file 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 fa81dea..e447206 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 @@ -45,15 +45,16 @@ fun LexiconTheme( ) { val density = LocalDensity.current val lexiconTheme = remember(density) { + val colorScheme = when (darkTheme) { + true -> darkColorScheme() + else -> lightColorScheme() + } LexiconTheme( animation = lexiconAnimation(), - colorScheme = when (darkTheme) { - true -> darkColorScheme() - else -> lightColorScheme() - }, + colorScheme = colorScheme, shapes = lexiconShapes(), dimens = lexiconDimen(density = density), - typography = lexiconTypography(), + typography = lexiconTypography(colorScheme = colorScheme), ) } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/theme/colors/LexiconColors.kt b/app/src/main/java/com/pixelized/rplexicon/ui/theme/colors/LexiconColors.kt index 3bf4463..23452eb 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/theme/colors/LexiconColors.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/theme/colors/LexiconColors.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import com.google.accompanist.placeholder.PlaceholderHighlight @Stable @Immutable @@ -20,6 +21,7 @@ data class LexiconColors( val rollOverlayBrush: Color, val dice: Dice, val characterSheet: CharacterSheet, + val search: Search, ) { @Stable data class Dice( @@ -33,6 +35,18 @@ data class LexiconColors( val innerBorder: Color, val outlineBorder: Color, ) + + @Stable + data class Search( + val chip: Chip, + val highlight: Color, + ) { + @Stable + data class Chip( + val selected: Color, + val unSelected: Color, + ) + } } @Stable @@ -94,6 +108,13 @@ fun colorScheme( ) ), sheet: LexiconColors.CharacterSheet, + search: LexiconColors.Search = LexiconColors.Search( + highlight = base.primary, + chip = LexiconColors.Search.Chip( + selected = base.onSurface, + unSelected = base.onSurface.copy(alpha = 0.5f), + ), + ), ) = LexiconColors( base = base, shadow = shadow, @@ -104,4 +125,5 @@ fun colorScheme( rollOverlayBrush = rollOverlayBrush, dice = dice, characterSheet = sheet, + search = search, ) diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/theme/shape/LexiconShapes.kt b/app/src/main/java/com/pixelized/rplexicon/ui/theme/shape/LexiconShapes.kt index 7f1d0ab..f4ed566 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/theme/shape/LexiconShapes.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/theme/shape/LexiconShapes.kt @@ -1,8 +1,8 @@ package com.pixelized.rplexicon.ui.theme.shape -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Shapes import androidx.compose.runtime.Stable +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape @Stable @@ -14,7 +14,7 @@ data class LexiconShapes( @Stable fun lexiconShapes( base: Shapes = Shapes(), - textField: Shape = CircleShape, + textField: Shape = RectangleShape, ) = LexiconShapes( base = base, textField = textField, diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/theme/typography/LexiconTypography.kt b/app/src/main/java/com/pixelized/rplexicon/ui/theme/typography/LexiconTypography.kt index b58bebf..d4fc083 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/theme/typography/LexiconTypography.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/theme/typography/LexiconTypography.kt @@ -6,11 +6,13 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.BaselineShift import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.sp import com.pixelized.rplexicon.R +import com.pixelized.rplexicon.ui.theme.colors.LexiconColors @Stable val zallFontFamily = FontFamily( @@ -25,33 +27,100 @@ val stampFontFamily = FontFamily( @Suppress("MemberVisibilityCanBePrivate") @Stable data class LexiconTypography( - val base: Typography = Typography(), - val stamp: TextStyle = base.headlineLarge.copy( + val base: Typography, + val stamp: TextStyle, + val bodyMediumDropCap: SpanStyle, // TODO DropCap class + val titleMediumDropCap: SpanStyle, // TODO DropCap class + val titleLargeDropCap: SpanStyle, // TODO DropCap class + val headlineSmallDropCap: SpanStyle, // TODO DropCap class + val headlineLargeDropCap: SpanStyle, // TODO DropCap class + val detail: Detail, + val search: Search, +) { + @Stable + data class Detail( + val highlightStyle: SpanStyle + ) + + @Stable + data class Search( + val category: TextStyle, + val categoryHighlight: SpanStyle, + val title: TextStyle, + val titleHighlight: SpanStyle, + val extract: TextStyle, + val extractHighlight: SpanStyle, + val extractBold: SpanStyle, + ) +} + +fun lexiconTypography( + colorScheme: LexiconColors, + base: Typography = Typography(), + stamp: TextStyle = base.headlineLarge.copy( fontFamily = stampFontFamily, ), - val bodyMediumDropCap: SpanStyle = base.bodyMedium.toDropCapSpan( + bodyMediumDropCap: SpanStyle = base.bodyMedium.toDropCapSpan( sizeRatio = 1.8f, antiLetterSpacing = 1.sp, baselineShift = BaselineShift(-0.08f), ), - val titleMediumDropCap: SpanStyle = base.titleMedium.toDropCapSpan( + titleMediumDropCap: SpanStyle = base.titleMedium.toDropCapSpan( sizeRatio = 1.7f, antiLetterSpacing = 2.sp, baselineShift = BaselineShift(-0.04f), ), - val titleLargeDropCap: SpanStyle = base.titleLarge.toDropCapSpan( + titleLargeDropCap: SpanStyle = base.titleLarge.toDropCapSpan( sizeRatio = 1.6f, antiLetterSpacing = 2.sp, baselineShift = BaselineShift(-0.04f), ), - val headlineSmallDropCap: SpanStyle = base.headlineSmall.toDropCapSpan( + headlineSmallDropCap: SpanStyle = base.headlineSmall.toDropCapSpan( antiLetterSpacing = 4.sp, sizeRatio = 1.5f, ), - val headlineLargeDropCap: SpanStyle = base.headlineLarge.toDropCapSpan( + headlineLargeDropCap: SpanStyle = base.headlineLarge.toDropCapSpan( antiLetterSpacing = 4.sp, sizeRatio = 1.4f, ), + detail: LexiconTypography.Detail = LexiconTypography.Detail( + highlightStyle = SpanStyle(color = colorScheme.search.highlight), + ), + search: LexiconTypography.Search = LexiconTypography.Search( + category = base.labelSmall.copy( + fontWeight = FontWeight.Light, + fontStyle = FontStyle.Italic, + ), + categoryHighlight = base.labelSmall.copy( + color = colorScheme.search.highlight, + fontWeight = FontWeight.Normal, + fontStyle = FontStyle.Italic, + ).toSpanStyle(), + title = base.titleMedium, + titleHighlight = base.titleMedium.copy( + color = colorScheme.search.highlight, + ).toSpanStyle(), + extract = base.labelSmall.copy( + fontWeight = FontWeight.Normal, + ), + extractHighlight = base.labelSmall.copy( + color = colorScheme.search.highlight, + fontWeight = FontWeight.Bold, + ).toSpanStyle(), + extractBold = base.labelSmall.copy( + fontWeight = FontWeight.Black, + ).toSpanStyle(), + ), +): LexiconTypography = LexiconTypography( + base = base, + stamp = stamp, + bodyMediumDropCap = bodyMediumDropCap, + titleMediumDropCap = titleMediumDropCap, + titleLargeDropCap = titleLargeDropCap, + headlineSmallDropCap = headlineSmallDropCap, + headlineLargeDropCap = headlineLargeDropCap, + detail = detail, + search = search, ) private fun TextStyle.toDropCapSpan( @@ -66,5 +135,3 @@ private fun TextStyle.toDropCapSpan( baselineShift = baselineShift ?: this.baselineShift ).toSpanStyle() } - -fun lexiconTypography(): LexiconTypography = LexiconTypography() \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/AnnotatedStringHelper.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/AnnotatedStringHelper.kt index e6d5772..a8a8361 100644 --- a/app/src/main/java/com/pixelized/rplexicon/utilitary/AnnotatedStringHelper.kt +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/AnnotatedStringHelper.kt @@ -1,22 +1,133 @@ package com.pixelized.rplexicon.utilitary +import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import com.pixelized.rplexicon.utilitary.extentions.annotatedSpan -private val dropCapRegex = Regex( - pattern = "(?:^|\n\n)([A-Z])" +private val String.highlightPattern: String + get() = Regex.escape(this) + +private val List.highlightPattern: String + get() = joinToString(separator = "|") { "(${Regex.escape(it)})" } + +private val List.extractWordPattern: String + get() = "(\\w*(?:${joinToString(separator = "|") { Regex.escape(it) }})\\w*)" + +private val List.extractSentencePattern: String + get() = "\\b[^.?!]*(?:${joinToString(separator = "|") { Regex.escape(it) }})[^.?!]*(?:[.?!]+|\$)" + +private val dropCapPattern: String + get() = "(?:^|\n\n)([A-Z])" + +// Specific optimization for the DropCap regex. +private val dropCapRegex = Regex(pattern = dropCapPattern) + +@Immutable +@Stable +class AnnotateStyle( + val regex: Regex, + val style: SpanStyle, ) +@Stable +infix fun Regex.styleWith(style: SpanStyle) = AnnotateStyle(regex = this, style = style) + +@Stable +fun annotate( + text: String, + vararg styles: AnnotateStyle, +) = AnnotatedString( + text = text, + spanStyles = styles.flatMap { + it.regex.annotatedSpan( + input = text, + style = it.style, + ) + }, +) + +@Stable +fun nullableAnnotate( + text: String?, + vararg styles: AnnotateStyle, +): AnnotatedString? = text?.let { + AnnotatedString( + text = text, + spanStyles = styles.flatMap { + it.regex.annotatedSpan( + input = text, + style = it.style, + ) + }, + ) +} + @Stable fun annotateWithDropCap( text: String, style: SpanStyle, -) = AnnotatedString( +) = annotate( text = text, - spanStyles = dropCapRegex.annotatedSpan( - input = text, - spanStyle = style, + dropCapRegex styleWith style, +) + +@Stable +fun dropCapRegex(): Regex = dropCapRegex + +/** + * Helper method to build a [Regex] to find [term] inside a [String]. + * This should be use to highlight any term contained in a [String]. + * @param term a word to find with the Regex + * @return a [Regex] instance + */ +@Stable +fun highlightRegex(term: String): Regex { + return Regex( + pattern = term.highlightPattern, + option = RegexOption.IGNORE_CASE, ) -) \ No newline at end of file +} + +/** + * Helper method to build a [Regex] to find [terms] inside a [String]. + * This should be use to highlight any term contained in a [String]. + * @param terms the list of word to find with the Regex + * @return a [Regex] instance + */ +@Stable +fun highlightRegex(terms: List): Regex { + return Regex( + pattern = terms.highlightPattern, + option = RegexOption.IGNORE_CASE, + ) +} + +/** + * Helper method to build a [Regex] to find [terms] inside a [String]. + * This should be use to extract a word with at least one highlighted term. + * @param terms the list of word to find with the Regex + * @return a [Regex] instance + */ +@Stable +fun extractWordRegex(terms: List): Regex { + return Regex( + pattern = terms.extractWordPattern, + option = RegexOption.IGNORE_CASE, + ) +} + +/** + * Helper method to build a [Regex] to find [terms] inside a [String]. + * This should be use to extract a sentence with at least one highlighted term. + * @param terms the list of word to find with the Regex + * @return a [Regex] instance + */ +@Stable +fun extractSentenceRegex(terms: List): Regex { + return Regex( + pattern = terms.extractSentencePattern, + option = RegexOption.IGNORE_CASE, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/Const.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/Const.kt index 9c92c2e..8e5427b 100644 --- a/app/src/main/java/com/pixelized/rplexicon/utilitary/Const.kt +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/Const.kt @@ -1,4 +1,8 @@ package com.pixelized.rplexicon.utilitary +//⟡ const val LOS_FULL = "⬧" -const val LOS_HOLLOW = "⬨" \ No newline at end of file +const val LOS_S_FULL = "⬥" +const val LOS_HOLLOW = "⬨" +const val LOS_S_HOLLOW = "⬦" +const val PUC_FULL = "•" \ 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 index 4fe6a80..95995e0 100644 --- a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/RegexEx.kt +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/RegexEx.kt @@ -1,52 +1,69 @@ package com.pixelized.rplexicon.utilitary.extentions +import androidx.compose.runtime.Stable import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle -fun Regex?.foldAll( - input: CharSequence?, - startIndex: Int = 0, -): String? = input?.let { +/** + * Helper method that build a nullable [String] by collapsing all result of the this [Regex] applied to a [input] [CharSequence] + * @param input the [String] to apply this [Regex] to. + * @return a nullable [String] instance. + */ +@Stable +fun Regex.foldAll( + input: CharSequence, +): String? { 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}" + return findAll(input) + .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 { + when (it.length) { + input.length -> it + else -> "$it..." + } } - previous = item - dummy - } -}?.takeIf { it.isNotEmpty() }?.let { - if (it.length == input.length) { - it - } else { - "$it..." - } } +@Stable +fun Regex?.annotatedString( + input: String, + style: SpanStyle, + startIndex: Int = 0, +): AnnotatedString = AnnotatedString( + text = input, + spanStyles = annotatedSpan( + input = input, + style = style, + startIndex = startIndex, + ), +) + +@Stable fun Regex?.annotatedSpan( input: String, - startIndex: Int = 0, - spanStyle: SpanStyle -): List> { - return this?.findAll(input = input, startIndex = startIndex)?.map { + style: SpanStyle, + startIndex: Int = 0 +): List> = this + ?.findAll( + input = input, + startIndex = startIndex + ) + ?.map { AnnotatedString.Range( - item = spanStyle, + item = style, start = it.range.first, end = it.range.last + 1 ) - }?.toList() ?: emptyList() -} - -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 + } + ?.toList() + ?: emptyList() 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 index dbf5e62..73c4a39 100644 --- a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/StringEx.kt +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/StringEx.kt @@ -3,37 +3,15 @@ package com.pixelized.rplexicon.utilitary.extentions import android.net.Uri import androidx.compose.runtime.Stable import androidx.core.net.toUri +import com.pixelized.rplexicon.utilitary.PUC_FULL val String.ARG: String get() = "$this={$this}" -@Stable -val String?.highlightRegex: Regex? - get() = this?.takeIf { it.isNotEmpty() }?.let { - Regex( - pattern = Regex.escape(it), - option = RegexOption.IGNORE_CASE, - ) - } +fun String?.prefix(prefix: String, link: Boolean = true): String? = + this?.let { "$prefix ${if (link) PUC_FULL else ""} $it" } -@Stable -val List.highlightRegex: Regex? - get() = if (isNotEmpty()) { - Regex( - pattern = joinToString(separator = "|") { "(${Regex.escape(it)})" }, - option = RegexOption.IGNORE_CASE, - ) - } else { - null - } - -@Stable -val List.finderRegex: Regex? - get() = if (isNotEmpty()) { - Regex( - pattern = joinToString(separator = "|") { "(\\w*.{0,30}${Regex.escape(it)}\\w*.{0,30}\\w*)" }, - option = RegexOption.IGNORE_CASE, - ) - } else null +fun String?.suffix(suffix: String, link: Boolean = true): String? = + this?.let { "$it ${if (link) PUC_FULL else ""} $suffix" } @Stable fun String?.toUriOrNull(): Uri? = try { diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index f08937a..75f35a1 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -50,14 +50,19 @@ Portrait : Rechercher - Rechercher - Race - Sexe - Statut : - Localisation : - Description : - Histoire : - Mots clés : + Lexique + Quête + Carte + Statut : + Localisation : + Description : + Histoire : + Mots clés : + Owner : + Location : + Individual reward : + Group reward : + Description : Détails de quête Complétée diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1b82d85..43e4cc9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -50,14 +50,21 @@ Portrait: Search - Search - Race - Gender - Status: - Location: - Description: - History: - Tags: + Lexicon + Quest + Location + Status: + Location: + Description: + History: + Tags: + Owner: + Location: + Individual reward: + Group reward: + Description: + Description: + Destination: Quest details Completed diff --git a/build.gradle.kts b/build.gradle.kts index 58f5e0f..9ec20cf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.application") version "8.1.3" apply false + id("com.android.application") version "8.1.4" apply false id("org.jetbrains.kotlin.android") version "1.9.10" apply false id("com.google.gms.google-services") version "4.3.14" apply false id("com.google.dagger.hilt.android") version "2.44" apply false