From af5fb8f33c118dfd7db1f366482e03cf2315ea34 Mon Sep 17 00:00:00 2001 From: "Andres Gomez, Thomas (ITDV CC) - AF (ext)" Date: Tue, 18 Jul 2023 21:21:52 +0200 Subject: [PATCH] Basic search implementation. --- app/build.gradle.kts | 3 + app/src/main/AndroidManifest.xml | 9 +- .../com/pixelized/rplexicon/MainActivity.kt | 22 +- .../ui/composable/FloatingActionButton.kt | 102 +++------ .../ui/composable/form/DropDownField.kt | 168 ++++++++++++++ .../rplexicon/ui/composable/form/TextField.kt | 109 +++++++++ .../authentication/AuthenticationScreen.kt | 26 +-- .../screens/detail/CharacterDetailScreen.kt | 49 ++-- .../ui/screens/lexicon/LexiconItem.kt | 91 ++++---- .../ui/screens/lexicon/LexiconScreen.kt | 74 +++--- .../ui/screens/search/SearchScreen.kt | 212 ++++++++++-------- .../ui/screens/search/SearchViewModel.kt | 117 ++++++++++ .../com/pixelized/rplexicon/ui/theme/Theme.kt | 16 +- .../{Color.kt => colors/LexiconColors.kt} | 14 +- .../rplexicon/ui/theme/shape/LexiconShapes.kt | 21 ++ .../rplexicon/utilitary/GlideHelp.kt | 40 ++++ ..._drop_down_24.xml => ic_arrow_down_24.xml} | 2 +- app/src/main/res/drawable/ic_clear_24.xml | 5 + app/src/main/res/values-fr/strings.xml | 9 +- app/src/main/res/values/strings.xml | 9 +- app/src/main/res/values/themes.xml | 6 +- 21 files changed, 806 insertions(+), 298 deletions(-) create mode 100644 app/src/main/java/com/pixelized/rplexicon/ui/composable/form/DropDownField.kt create mode 100644 app/src/main/java/com/pixelized/rplexicon/ui/composable/form/TextField.kt create mode 100644 app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchViewModel.kt rename app/src/main/java/com/pixelized/rplexicon/ui/theme/{Color.kt => colors/LexiconColors.kt} (75%) create mode 100644 app/src/main/java/com/pixelized/rplexicon/ui/theme/shape/LexiconShapes.kt create mode 100644 app/src/main/java/com/pixelized/rplexicon/utilitary/GlideHelp.kt rename app/src/main/res/drawable/{ic_baseline_arrow_drop_down_24.xml => ic_arrow_down_24.xml} (79%) create mode 100644 app/src/main/res/drawable/ic_clear_24.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 07eb31b..fbcf239 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -97,6 +97,9 @@ dependencies { implementation("com.google.accompanist:accompanist-navigation-animation:0.30.1") implementation("com.google.accompanist:accompanist-placeholder:0.30.1") + // Splash Screen support prior to Android 12 + implementation("androidx.core:core-splashscreen:1.0.1") + // Google service implementation("com.google.android.gms:play-services-auth:20.6.0") implementation( diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 16743da..6f1b495 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,19 +9,23 @@ + + android:screenOrientation="portrait" + android:theme="@style/Theme.Lexicon.Starting" + android:windowSoftInputMode="adjustResize"> @@ -29,5 +33,4 @@ - \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/MainActivity.kt b/app/src/main/java/com/pixelized/rplexicon/MainActivity.kt index 8743622..2bcd803 100644 --- a/app/src/main/java/com/pixelized/rplexicon/MainActivity.kt +++ b/app/src/main/java/com/pixelized/rplexicon/MainActivity.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme @@ -15,13 +16,19 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.core.view.WindowCompat import com.pixelized.rplexicon.ui.navigation.ScreenNavHost import com.pixelized.rplexicon.ui.theme.LexiconTheme import dagger.hilt.android.AndroidEntryPoint -val LocalActivity = staticCompositionLocalOf { error("Activity not available") } -val LocalSnack = - staticCompositionLocalOf { error("SnackbarHostState not available") } +val LocalActivity = staticCompositionLocalOf { + error("Activity not available") +} +val LocalSnack = staticCompositionLocalOf { + error("SnackbarHostState not available") +} +val NO_WINDOW_INSETS = WindowInsets(0, 0, 0, 0) @AndroidEntryPoint class MainActivity : ComponentActivity() { @@ -29,6 +36,14 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + // Disable system inset consuming. + WindowCompat.setDecorFitsSystemWindows(window, false) + + // splashscreen management + installSplashScreen().apply { + setKeepOnScreenCondition { false } + } + setContent { LexiconTheme { CompositionLocalProvider( @@ -36,6 +51,7 @@ class MainActivity : ComponentActivity() { LocalSnack provides remember { SnackbarHostState() } ) { Scaffold( + contentWindowInsets = NO_WINDOW_INSETS, content = { padding -> Surface( modifier = Modifier diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/composable/FloatingActionButton.kt b/app/src/main/java/com/pixelized/rplexicon/ui/composable/FloatingActionButton.kt index b82235d..2e1fcbb 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/composable/FloatingActionButton.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/composable/FloatingActionButton.kt @@ -5,15 +5,14 @@ import androidx.compose.animation.core.* import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.* +import androidx.compose.material3.* import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -25,24 +24,24 @@ import com.pixelized.rplexicon.ui.theme.LexiconTheme @Composable fun FloatingActionButton( - modifier: Modifier = Modifier, - expended: Boolean, - enabled: Boolean = true, - innerSpacing: Dp = 16.dp, - contentPadding: PaddingValues = FlyingBlueFloatingActionButtonDefault.ContentPadding, - elevation: ButtonElevation? = ButtonDefaults.elevation( - defaultElevation = 0.dp, - pressedElevation = 0.dp, - disabledElevation = 0.dp, - ), - shape: Shape = CircleShape, - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - colors: ButtonColors = ButtonDefaults.buttonColors(), onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + shape: Shape = ButtonDefaults.outlinedShape, + colors: ButtonColors = ButtonDefaults.outlinedButtonColors(), + elevation: ButtonElevation? = null, + border: BorderStroke? = BorderStroke( + width = 2.dp, + color = MaterialTheme.colorScheme.primary, + ), + contentPadding: PaddingValues = PaddingValues(all = 0.dp), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + expended: Boolean, + innerSpacing: Dp = 8.dp, icon: @Composable (RowScope.() -> Unit), text: @Composable (RowScope.() -> Unit), ) { - LocalButton( + OutlinedButton( modifier = modifier, onClick = onClick, enabled = enabled, @@ -51,13 +50,16 @@ fun FloatingActionButton( shape = shape, colors = colors, contentPadding = contentPadding, + border = border, content = { - FabContent( - expended = expended, - innerSpacing = innerSpacing, - icon = icon, - text = text, - ) + BoxWithConstraints { + FabContent( + expended = expended, + innerSpacing = innerSpacing, + icon = icon, + text = text, + ) + } }, ) } @@ -69,9 +71,10 @@ private fun BoxWithConstraintsScope.FabContent( icon: @Composable (RowScope.() -> Unit), text: @Composable (RowScope.() -> Unit), ) { + val maxWidth = if (LocalView.current.isInEditMode) 300.dp else maxWidth val width by animateDpAsState( label = "FabContentWidth", - targetValue = if (expended) maxWidth else minWidth, + targetValue = if (expended) maxWidth else 56.dp, animationSpec = when (expended) { true -> tween(durationMillis = 300, easing = FastOutSlowInEasing) else -> tween(durationMillis = 300, delayMillis = 100, easing = FastOutSlowInEasing) @@ -105,56 +108,6 @@ private fun BoxWithConstraintsScope.FabContent( } } -@OptIn(ExperimentalMaterialApi::class) -@Composable -private fun LocalButton( - onClick: () -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - elevation: ButtonElevation? = ButtonDefaults.elevation(), - shape: Shape = MaterialTheme.shapes.small, - border: BorderStroke? = null, - colors: ButtonColors = ButtonDefaults.buttonColors(), - contentPadding: PaddingValues = FlyingBlueFloatingActionButtonDefault.ContentPadding, - content: @Composable BoxWithConstraintsScope.() -> Unit -) { - val contentColor by colors.contentColor(enabled) - Surface( - onClick = onClick, - modifier = modifier, - enabled = enabled, - shape = shape, - color = colors.backgroundColor(enabled).value, - contentColor = contentColor.copy(alpha = 1f), - border = border, - elevation = elevation?.elevation(enabled, interactionSource)?.value ?: 0.dp, - interactionSource = interactionSource, - ) { - CompositionLocalProvider(LocalContentAlpha provides contentColor.alpha) { - ProvideTextStyle( - value = MaterialTheme.typography.button - ) { - BoxWithConstraints( - Modifier - .defaultMinSize( - minWidth = FlyingBlueFloatingActionButtonDefault.MinWidth, - minHeight = FlyingBlueFloatingActionButtonDefault.MinHeight - ) - .padding(contentPadding), - content = content - ) - } - } - } -} - -object FlyingBlueFloatingActionButtonDefault { - val ContentPadding = PaddingValues(all = 0.dp) - val MinWidth = 56.dp - val MinHeight = 56.dp -} - @Composable @Preview private fun FloatingActionButtonPreview( @@ -162,6 +115,7 @@ private fun FloatingActionButtonPreview( ) { LexiconTheme { FloatingActionButton( + modifier = Modifier.padding(all = 16.dp), expended = expended, icon = { Icon( diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/composable/form/DropDownField.kt b/app/src/main/java/com/pixelized/rplexicon/ui/composable/form/DropDownField.kt new file mode 100644 index 0000000..868b47f --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/composable/form/DropDownField.kt @@ -0,0 +1,168 @@ +package com.pixelized.rplexicon.ui.composable.form + +import android.content.res.Configuration.UI_MODE_NIGHT_NO +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.annotation.StringRes +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.with +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.pixelized.rplexicon.R +import com.pixelized.rplexicon.ui.theme.LexiconTheme +import com.pixelized.rplexicon.utilitary.extentions.lexicon + +@Stable +data class DropDownFieldUio( + @StringRes val label: Int, + val values: List>, + val value: State>, + val onValueChange: (T?, String) -> Unit, +) { + companion object { + fun preview(@StringRes label: Int, id: T?, value: String) = DropDownFieldUio( + label = label, + values = emptyList(), + value = mutableStateOf(id to value), + onValueChange = { _, _ -> }, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class) +@Composable +fun DropDownField( + modifier: Modifier = Modifier, + field: DropDownFieldUio, +) { + var expended by remember(field) { mutableStateOf(false) } + + ExposedDropdownMenuBox( + modifier = modifier, + expanded = expended, + onExpandedChange = { expended = !expended && field.value.value.first == null }, + ) { + OutlinedTextField( + modifier = Modifier.menuAnchor(), + shape = MaterialTheme.lexicon.shapes.textField, + readOnly = true, + singleLine = true, + label = { + Text( + text = stringResource(id = field.label) + ) + }, + trailingIcon = { + AnimatedContent( + modifier = Modifier.size(size = 48.dp), + targetState = field.value.value.first != null, + transitionSpec = { fadeIn() with fadeOut() }, + label = "DropDownFieldTrailingIconAnimation", + ) { + when (it) { + true -> IconButton( + onClick = { field.onValueChange(null, "") }, + ) { + Icon( + modifier = Modifier.size(size = 18.dp), + painter = painterResource(id = R.drawable.ic_clear_24), + contentDescription = null, + ) + } + + else -> Box( + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.size(size = 24.dp), + painter = painterResource(id = R.drawable.ic_arrow_down_24), + contentDescription = null, + ) + } + } + } + }, + colors = TextFieldDefaults.colors( + focusedContainerColor = MaterialTheme.colorScheme.surface, + unfocusedContainerColor = MaterialTheme.colorScheme.surface, + ), + value = field.value.value.second, + onValueChange = {}, + ) + + DropdownMenu( + expanded = expended, + onDismissRequest = { expended = false }, + ) { + field.values.forEach { + val label = stringResource(id = it.second) + DropdownMenuItem( + onClick = { + expended = false + field.onValueChange(it.first, label) + }, + text = { + Text(text = label) + }, + ) + } + } + } +} + +@Composable +@Preview(uiMode = UI_MODE_NIGHT_NO) +@Preview(uiMode = UI_MODE_NIGHT_YES) +private fun DropDownFieldPreview( + @PreviewParameter(DropDownFieldPreviewProvider::class) preview: Pair, +) { + LexiconTheme { + Surface { + DropDownField( + modifier = Modifier + .fillMaxWidth() + .padding(all = 8.dp), + field = DropDownFieldUio( + label = R.string.lexicon_search, + values = emptyList(), + value = remember { mutableStateOf(preview) }, + onValueChange = { _, _ -> }, + ) + ) + } + } +} + +private class DropDownFieldPreviewProvider : PreviewParameterProvider> { + override val values: Sequence> = sequenceOf(null to "", "" to "preview") +} \ No newline at end of file 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/TextField.kt new file mode 100644 index 0000000..7cce867 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/composable/form/TextField.kt @@ -0,0 +1,109 @@ +package com.pixelized.rplexicon.ui.composable.form + +import android.content.res.Configuration.UI_MODE_NIGHT_NO +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.annotation.StringRes +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.pixelized.rplexicon.R +import com.pixelized.rplexicon.ui.theme.LexiconTheme +import com.pixelized.rplexicon.utilitary.extentions.lexicon + +@Stable +data class TextFieldUio( + @StringRes val label: Int, + val value: State, + val onValueChange: (String) -> Unit, +) { + companion object { + fun preview(@StringRes label: Int) = TextFieldUio( + label = label, + value = mutableStateOf(""), + onValueChange = {}, + ) + } +} + +@Composable +fun TextField( + modifier: Modifier = Modifier, + field: TextFieldUio, +) { + OutlinedTextField( + modifier = modifier, + shape = MaterialTheme.lexicon.shapes.textField, + colors = TextFieldDefaults.colors( + focusedContainerColor = MaterialTheme.colorScheme.surface, + unfocusedContainerColor = MaterialTheme.colorScheme.surface, + ), + label = { + Text(text = stringResource(id = field.label)) + }, + trailingIcon = { + AnimatedVisibility( + visible = field.value.value.isNotEmpty(), + enter = fadeIn(), + exit = fadeOut(), + ) { + IconButton(onClick = { field.onValueChange("") }) { + Icon( + modifier = Modifier.size(size = 18.dp), + painter = painterResource(id = R.drawable.ic_clear_24), + contentDescription = null, + ) + } + } + }, + value = field.value.value, + onValueChange = field.onValueChange, + ) +} + +@Composable +@Preview(uiMode = UI_MODE_NIGHT_NO) +@Preview(uiMode = UI_MODE_NIGHT_YES) +private fun TextFieldPreview( + @PreviewParameter(TextFieldPreviewProvider::class) preview: String, +) { + LexiconTheme { + Surface { + TextField( + modifier = Modifier + .fillMaxWidth() + .padding(all = 8.dp), + field = TextFieldUio( + label = R.string.lexicon_search, + value = remember { mutableStateOf(preview) }, + onValueChange = {}, + ) + ) + } + } +} + +private class TextFieldPreviewProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf("", "preview") +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/authentication/AuthenticationScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/authentication/AuthenticationScreen.kt index 42a8d9e..8c58235 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/authentication/AuthenticationScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/authentication/AuthenticationScreen.kt @@ -5,9 +5,9 @@ import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -16,13 +16,13 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight 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.systemBarsPadding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -71,9 +71,12 @@ fun AuthenticationScreen( val state = authenticationVM.rememberAuthenticationState() Surface { + PartyBackground() + AuthenticationScreenContent( modifier = Modifier .fillMaxSize() + .systemBarsPadding() .padding(all = 16.dp), version = versionVM.version, onGoogleSignIn = { @@ -108,22 +111,19 @@ private fun AuthenticationScreenContent( ) { val typography = MaterialTheme.typography - PartyBackground() - Column( modifier = modifier, verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Bottom), horizontalAlignment = Alignment.End, ) { - Button( + OutlinedButton( modifier = Modifier .fillMaxWidth() - .border( - width = 2.dp, - color = MaterialTheme.colorScheme.primary, - shape = CircleShape, - ), - colors = ButtonDefaults.outlinedButtonColors(), + .height(56.dp), + border = BorderStroke( + width = 2.dp, + color = MaterialTheme.colorScheme.primary, + ), onClick = onGoogleSignIn, ) { Text(text = rememeberGoogleStringResource()) diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailScreen.kt index 7609cef..8952d21 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailScreen.kt @@ -6,7 +6,6 @@ import android.net.Uri import androidx.annotation.StringRes import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -17,9 +16,9 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize 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.sizeIn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState @@ -40,6 +39,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.composed +import androidx.compose.ui.draw.alpha import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color @@ -47,6 +47,7 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorMatrix import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -58,8 +59,10 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.pixelized.rplexicon.R import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost import com.pixelized.rplexicon.ui.theme.LexiconTheme +import com.pixelized.rplexicon.utilitary.rememberLoadingTransition import com.skydoves.landscapist.ImageOptions import com.skydoves.landscapist.glide.GlideImage +import com.skydoves.landscapist.glide.GlideImageState @Stable data class CharacterDetailUio( @@ -83,7 +86,6 @@ fun CharacterDetailScreen( modifier = Modifier.fillMaxSize(), item = viewModel.character, onBack = { screen.popBackStack() }, - onImage = { }, ) } } @@ -95,7 +97,6 @@ private fun CharacterDetailScreenContent( state: ScrollState = rememberScrollState(), item: State, onBack: () -> Unit, - onImage: (Uri) -> Unit, ) { val colorScheme = MaterialTheme.colorScheme val typography = MaterialTheme.typography @@ -129,9 +130,16 @@ private fun CharacterDetailScreenContent( .aspectRatio(ratio = 1f) .scrollOffset(scrollState = state) { -it / 2 }, ) { + val transition = rememberLoadingTransition { uri } GlideImage( - modifier = Modifier.matchParentSize(), - imageModel = { uri.toString() }, + modifier = Modifier + .matchParentSize() + .alpha(alpha = transition.alpha), + onImageStateChanged = { + if (it is GlideImageState.Success) { + transition.target = 1f + } + }, imageOptions = ImageOptions( alignment = Alignment.TopCenter, contentScale = ContentScale.Crop, @@ -141,7 +149,8 @@ private fun CharacterDetailScreenContent( ) }, ), - previewPlaceholder = R.drawable.ic_empty, + imageModel = { uri.toString() }, + previewPlaceholder = R.drawable.im_brulkhai, ) Box( modifier = Modifier @@ -220,6 +229,8 @@ private fun CharacterDetailScreenContent( ) } if (item.value.portrait.isNotEmpty()) { + val configuration = LocalConfiguration.current + val maxSize = remember { (configuration.screenWidthDp.dp - 16.dp * 2) } Text( modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp), style = typography.titleMedium, @@ -230,15 +241,21 @@ private fun CharacterDetailScreenContent( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { items(items = item.value.portrait) { + val transition = rememberLoadingTransition { it } GlideImage( modifier = Modifier - .clickable { onImage(it) } - .height(320.dp), - imageModel = { it }, + .sizeIn(maxWidth = maxSize, maxHeight = maxSize) + .alpha(alpha = transition.alpha), + onImageStateChanged = { + if (it is GlideImageState.Success) { + transition.target = 1f + } + }, imageOptions = ImageOptions( contentScale = ContentScale.FillHeight ), - previewPlaceholder = R.drawable.ic_empty, + imageModel = { it }, + previewPlaceholder = R.drawable.im_brulkhai, ) } } @@ -285,15 +302,6 @@ private fun CharacterDetailScreenContentPreview() { race = R.string.race_half_orc, portrait = listOf( Uri.parse("https://cdnb.artstation.com/p/assets/images/images/003/024/889/large/bayard-wu-0716.jpg?1468642855"), - Uri.parse("https://cdnb.artstation.com/p/assets/images/images/003/024/877/large/bayard-wu-0714.jpg?1468642665"), - Uri.parse("https://cdnb.artstation.com/p/assets/images/images/003/024/887/large/bayard-wu-0715.jpg?1468642839"), - Uri.parse("https://cdnb.artstation.com/p/assets/images/images/003/024/891/large/bayard-wu-0623-03.jpg?1468642872"), - Uri.parse("https://cdna.artstation.com/p/assets/images/images/002/869/868/large/bayard-wu-0622-03.jpg?1466664135"), - Uri.parse("https://cdnb.artstation.com/p/assets/images/images/002/869/871/large/bayard-wu-0622-04.jpg?1466664153"), - Uri.parse("https://cdnb.artstation.com/p/assets/images/images/004/347/181/large/bayard-wu-1217.jpg?1482770883"), - Uri.parse("https://cdnb.artstation.com/p/assets/images/images/004/297/635/large/bayard-wu-1215.jpg?1482166826"), - Uri.parse("https://cdnb.artstation.com/p/assets/images/images/004/297/631/large/bayard-wu-1209.jpg?1482166803"), - Uri.parse("https://cdnb.artstation.com/p/assets/images/images/004/297/641/large/bayard-wu-1212.jpg?1482166838"), ), 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, @@ -304,7 +312,6 @@ private fun CharacterDetailScreenContentPreview() { modifier = Modifier.fillMaxSize(), item = character, onBack = { }, - onImage = { }, ) } } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconItem.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconItem.kt index 32ec9d9..03f62d2 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconItem.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconItem.kt @@ -55,49 +55,53 @@ fun LexiconItem( ) { val typography = MaterialTheme.typography - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(4.dp), - ) { - FlowRow( - horizontalArrangement = Arrangement.spacedBy(4.dp), + Surface(modifier = modifier) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp, horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), ) { - Text( - modifier = Modifier - .alignByBaseline() - .placeholder { item.placeholder }, - style = remember { typography.bodyLarge.copy(fontWeight = FontWeight.Bold) }, - maxLines = 1, - text = item.name, - ) - Text( - modifier = Modifier - .alignByBaseline() - .placeholder { item.placeholder }, - style = typography.labelMedium, - maxLines = 1, - text = item.diminutive ?: "" - ) - } - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - Text( - modifier = Modifier - .alignByBaseline() - .placeholder { item.placeholder }, - style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) }, - maxLines = 1, - text = stringResource(id = item.gender) - ) - Text( - modifier = Modifier - .alignByBaseline() - .placeholder { item.placeholder }, - style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) }, - maxLines = 1, - text = stringResource(id = item.race) - ) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + modifier = Modifier + .alignByBaseline() + .placeholder { item.placeholder }, + style = remember { typography.bodyLarge.copy(fontWeight = FontWeight.Bold) }, + maxLines = 1, + text = item.name, + ) + Text( + modifier = Modifier + .alignByBaseline() + .placeholder { item.placeholder }, + style = typography.labelMedium, + maxLines = 1, + text = item.diminutive ?: "" + ) + } + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + modifier = Modifier + .alignByBaseline() + .placeholder { item.placeholder }, + style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) }, + maxLines = 1, + text = stringResource(id = item.gender) + ) + Text( + modifier = Modifier + .alignByBaseline() + .placeholder { item.placeholder }, + style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) }, + maxLines = 1, + text = stringResource(id = item.race) + ) + } } } } @@ -110,9 +114,6 @@ private fun LexiconItemContentPreview() { LexiconTheme { Surface { LexiconItem( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp, horizontal = 16.dp), item = LexiconItemUio( id = 0, name = "Brulkhai", diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconScreen.kt index 449cf66..135fccd 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconScreen.kt @@ -7,6 +7,7 @@ import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -17,7 +18,9 @@ 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.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 @@ -30,6 +33,7 @@ import androidx.compose.material.pullrefresh.PullRefreshState import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -46,12 +50,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext +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.LocalSnack +import com.pixelized.rplexicon.NO_WINDOW_INSETS import com.pixelized.rplexicon.R +import com.pixelized.rplexicon.ui.composable.FloatingActionButton import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost import com.pixelized.rplexicon.ui.navigation.screens.navigateToCharacterDetail import com.pixelized.rplexicon.ui.navigation.screens.navigateToSearch @@ -100,6 +107,7 @@ fun LexiconScreen( Surface { LexiconScreenContent( + modifier = Modifier.systemBarsPadding(), items = viewModel.items, lazyColumnState = lazyListState, refreshState = refresh, @@ -139,35 +147,44 @@ private fun LexiconScreenContent( ) { Scaffold( modifier = modifier, + contentWindowInsets = NO_WINDOW_INSETS, topBar = { TopAppBar( + windowInsets = NO_WINDOW_INSETS, title = { Text(text = stringResource(id = R.string.app_name)) }, ) }, -// floatingActionButton = { -// FloatingActionButton( -// modifier = Modifier.padding(start = 32.dp), -// expended = isFabExpended.value, -// onClick = onSearch, -// icon = { -// Icon( -// tint = MaterialTheme.colorScheme.onPrimary, -// painter = painterResource(id = R.drawable.ic_baseline_search_24), -// contentDescription = null, -// ) -// }, -// text = { -// val typography = MaterialTheme.typography -// Text( -// color = MaterialTheme.colorScheme.onPrimary, -// style = remember { typography.bodyLarge.copy(fontWeight = FontWeight.Bold) }, -// text = "Rechercher", -// ) -// }, -// ) -// } + floatingActionButton = { + Box( + modifier = Modifier + .fillMaxWidth() + .height(56.dp) + .padding(start = 32.dp), // `Fix` Scaffold content size for FAB. + contentAlignment = Alignment.CenterEnd, + ) { + AnimatedVisibility( + visible = items.value.isNotEmpty(), + enter = fadeIn(), + exit = fadeOut(), + ) { + FloatingActionButton( + expended = isFabExpended.value, + onClick = onSearch, + icon = { + Icon( + painter = painterResource(id = R.drawable.ic_baseline_search_24), + contentDescription = null, + ) + }, + text = { + Text(text = stringResource(id = R.string.lexicon_search)) + }, + ) + } + } + } ) { padding -> Box( modifier = Modifier.padding(paddingValues = padding), @@ -186,15 +203,12 @@ private fun LexiconScreenContent( state = lazyColumnState, contentPadding = PaddingValues( top = 8.dp, - bottom = 8.dp + 16.dp // + 56.dp + 16.dp, + bottom = 8.dp + 16.dp + 56.dp + 16.dp, ), ) { items(count = 6) { LexiconItem( - modifier = Modifier - .animateItemPlacement() - .fillMaxWidth() - .padding(vertical = 8.dp, horizontal = 16.dp), + modifier = Modifier.animateItemPlacement(), item = LexiconItemUio.Brulkhai, ) } @@ -207,7 +221,7 @@ private fun LexiconScreenContent( state = lazyColumnState, contentPadding = PaddingValues( top = 8.dp, - bottom = 8.dp + 16.dp // + 56.dp + 16.dp, + bottom = 8.dp + 16.dp + 56.dp + 16.dp, ), ) { items( @@ -218,9 +232,7 @@ private fun LexiconScreenContent( LexiconItem( modifier = Modifier .animateItemPlacement() - .clickable { onItem(it) } - .fillMaxWidth() - .padding(vertical = 8.dp, horizontal = 16.dp), + .clickable { onItem(it) }, item = it, ) } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchScreen.kt index 70bac91..4efdcb9 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchScreen.kt @@ -2,46 +2,79 @@ package com.pixelized.rplexicon.ui.screens.search import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding -import androidx.compose.material.DropdownMenuItem -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ExposedDropdownMenuBox +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.TextField -import androidx.compose.material3.TextFieldDefaults 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.graphics.Color +import androidx.compose.ui.platform.LocalContext 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.model.Lexicon +import com.pixelized.rplexicon.ui.composable.form.DropDownField +import com.pixelized.rplexicon.ui.composable.form.DropDownFieldUio +import com.pixelized.rplexicon.ui.composable.form.TextField +import com.pixelized.rplexicon.ui.composable.form.TextFieldUio import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost +import com.pixelized.rplexicon.ui.navigation.screens.navigateToCharacterDetail +import com.pixelized.rplexicon.ui.screens.lexicon.LexiconItem +import com.pixelized.rplexicon.ui.screens.lexicon.LexiconItemUio import com.pixelized.rplexicon.ui.theme.LexiconTheme +@Stable +data class SearchFormUio( + val search: TextFieldUio, + val gender: DropDownFieldUio, + val race: DropDownFieldUio, +) + @Composable -fun SearchScreen() { +fun SearchScreen( + viewModel: SearchViewModel = hiltViewModel(), +) { val screen = LocalScreenNavHost.current Surface { SearchScreenContent( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .systemBarsPadding() + .imePadding(), + items = viewModel.filter, + form = viewModel.form, + onItem = { + screen.navigateToCharacterDetail(id = it.id) + }, onBack = { screen.popBackStack() } @@ -49,17 +82,23 @@ fun SearchScreen() { } } -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable private fun SearchScreenContent( modifier: Modifier = Modifier, + lazyColumnState: LazyListState = rememberLazyListState(), + items: State>, + form: SearchFormUio, onBack: () -> Unit, + onItem: (LexiconItemUio) -> Unit, ) { Scaffold( modifier = modifier, + contentWindowInsets = NO_WINDOW_INSETS, containerColor = Color.Transparent, topBar = { TopAppBar( + windowInsets = NO_WINDOW_INSETS, navigationIcon = { IconButton(onClick = onBack) { Icon( @@ -69,99 +108,52 @@ private fun SearchScreenContent( } }, title = { - Text(text = "Rechercher") + Text(text = stringResource(id = R.string.search_field_title)) }, ) }, - ) { - Column( + ) { paddingValues -> + LazyColumn( modifier = Modifier - .padding(paddingValues = it) - .padding(all = 16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), + .fillMaxSize() + .padding(paddingValues = paddingValues), + state = lazyColumnState, + contentPadding = PaddingValues(vertical = 8.dp), ) { - TextField( - modifier = Modifier.fillMaxWidth(), - value = "", - label = { - Text("Nom") - }, - onValueChange = { _ -> }, - colors = TextFieldDefaults.colors( - focusedContainerColor = MaterialTheme.colorScheme.surface, - unfocusedContainerColor = MaterialTheme.colorScheme.surface, - ), - ) - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - DropDownField( - modifier = Modifier.weight(1f), - subject = remember { mutableStateOf("1") }, - subjects = listOf("1", "2"), - onChange = { }, - expanded = remember { mutableStateOf(false) }, - onExpandedChange = { } - ) - DropDownField( - modifier = Modifier.weight(1f), - subject = remember { mutableStateOf("1") }, - subjects = listOf("1", "2"), - onChange = { }, - expanded = remember { mutableStateOf(false) }, - onExpandedChange = { } - ) + item { + Column( + modifier = Modifier.padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + TextField( + modifier = Modifier.fillMaxWidth(), + field = form.search, + ) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + DropDownField( + modifier = Modifier.weight(1f), + field = form.gender, + ) + DropDownField( + modifier = Modifier.weight(1f), + field = form.race, + ) + } + Divider(modifier = Modifier.padding(top = 16.dp, bottom = 8.dp)) + } } - } - } -} - -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun DropDownField( - modifier: Modifier = Modifier, - subjects: List, - subject: State, - onChange: (String) -> Unit, - expanded: State, - onExpandedChange: (Boolean) -> Unit -) { - ExposedDropdownMenuBox( - modifier = modifier, - expanded = expanded.value, - onExpandedChange = onExpandedChange, - ) { - TextField( - modifier = Modifier.clickable { onExpandedChange(true) }, - readOnly = true, - singleLine = true, - placeholder = { - Text( - text = "pouet" - ) - }, - trailingIcon = { - Icon( - painter = painterResource(id = R.drawable.ic_baseline_arrow_drop_down_24), - contentDescription = null, - ) - }, - colors = TextFieldDefaults.colors( - focusedContainerColor = MaterialTheme.colorScheme.surface, - unfocusedContainerColor = MaterialTheme.colorScheme.surface, - ), - value = subject.value, - onValueChange = {}, - ) - - ExposedDropdownMenu( - expanded = expanded.value, - onDismissRequest = { onExpandedChange(false) }, - ) { - subjects.forEach { - DropdownMenuItem( - onClick = { onChange(it) }, - content = { Text(text = it) }, + items( + items = items.value, + key = { it.id }, + contentType = { "Lexicon" }, + ) { + LexiconItem( + modifier = Modifier + .animateItemPlacement() + .clickable { onItem(it) }, + item = it, ) } } @@ -173,9 +165,37 @@ fun DropDownField( @Preview(uiMode = UI_MODE_NIGHT_YES) private fun SearchScreenContentPreview() { LexiconTheme { + val context = LocalContext.current Surface { SearchScreenContent( modifier = Modifier.fillMaxSize(), + form = SearchFormUio( + search = TextFieldUio.preview(R.string.search_field_search), + gender = DropDownFieldUio.preview( + label = R.string.search_field_gender, + id = Lexicon.Gender.FEMALE, + value = context.getString(R.string.gender_female), + ), + race = DropDownFieldUio.preview( + label = R.string.search_field_race, + id = null, + value = "", + ), + ), + items = remember { + mutableStateOf( + listOf( + LexiconItemUio( + id = 0, + name = "Brulkhai", + diminutive = "Bru", + gender = R.string.gender_female_short, + race = R.string.race_half_orc, + ) + ) + ) + }, + onItem = { }, onBack = { }, ) } 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..78f44dd --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchViewModel.kt @@ -0,0 +1,117 @@ +package com.pixelized.rplexicon.ui.screens.search + +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import com.pixelized.rplexicon.R +import com.pixelized.rplexicon.model.Lexicon +import com.pixelized.rplexicon.model.Lexicon.Gender +import com.pixelized.rplexicon.model.Lexicon.Race +import com.pixelized.rplexicon.repository.LexiconRepository +import com.pixelized.rplexicon.ui.composable.form.DropDownFieldUio +import com.pixelized.rplexicon.ui.composable.form.TextFieldUio +import com.pixelized.rplexicon.ui.screens.lexicon.LexiconItemUio +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class SearchViewModel @Inject constructor( + private val repository: LexiconRepository, +) : ViewModel() { + private val search = mutableStateOf("") + private val gender = mutableStateOf>(null to "") + private val race = mutableStateOf>(null to "") + + val form = SearchFormUio( + search = TextFieldUio( + label = R.string.search_field_search, + value = search, + onValueChange = { + search.value = it + } + ), + gender = DropDownFieldUio( + label = R.string.search_field_gender, + values = genders(), + value = gender, + onValueChange = { id, value -> gender.value = id to value }, + ), + race = DropDownFieldUio( + label = R.string.search_field_race, + values = races(), + value = race, + onValueChange = { id, value -> race.value = id to value }, + ), + ) + + private var data: List = repository.data.value + + val filter = derivedStateOf { + data + .filter { item -> + val gender = gender.value.first?.let { it == item.gender } + val race = race.value.first?.let { it == item.race } + val search = search.value.takeIf { it.isNotEmpty() }?.let { + val name = item.name.contains(search.value, true) + val diminutive = item.diminutive?.contains(search.value, true) == true + val description = item.description?.contains(search.value, true) == true + val history = item.history?.contains(search.value, true) == true + name || diminutive || description || history + } + (gender == null || gender) && (race == null || race) && (search == null || search) + }.map { + LexiconItemUio( + id = it.id, + name = it.name, + diminutive = it.diminutive?.takeIf { it.isNotBlank() }?.let { "./ $it" }, + gender = when (it.gender) { + Gender.MALE -> R.string.gender_male_short + Gender.FEMALE -> R.string.gender_female_short + Gender.UNDETERMINED -> R.string.gender_undetermined_short + }, + race = when (it.race) { + Race.ELF -> R.string.race_elf + Race.HALFLING -> R.string.race_halfling + Race.HUMAN -> R.string.race_human + Race.DWARF -> R.string.race_dwarf + Race.HALF_ELF -> R.string.race_half_elf + Race.HALF_ORC -> R.string.race_half_orc + Race.DRAGONBORN -> R.string.race_dragonborn + Race.GNOME -> R.string.race_gnome + Race.TIEFLING -> R.string.race_tiefling + Race.AARAKOCRA -> R.string.race_aarakocra + Race.GENASI -> R.string.race_genasi + Race.DEEP_GNOME -> R.string.race_deep_gnome + Race.GOLIATH -> R.string.race_goliath + Race.UNDETERMINED -> R.string.race_undetermined + }, + ) + } + .sortedBy { it.name } + } + + companion object { + private fun genders() = listOf( + Gender.MALE to R.string.gender_male, + Gender.FEMALE to R.string.gender_female, + Gender.UNDETERMINED to R.string.gender_undetermined, + ) + + private fun races() = listOf( + Race.ELF to R.string.race_elf, + Race.HALFLING to R.string.race_halfling, + Race.HUMAN to R.string.race_human, + Race.DWARF to R.string.race_dwarf, + Race.HALF_ELF to R.string.race_half_elf, + Race.HALF_ORC to R.string.race_half_orc, + Race.DRAGONBORN to R.string.race_dragonborn, + Race.GNOME to R.string.race_gnome, + Race.TIEFLING to R.string.race_tiefling, + Race.AARAKOCRA to R.string.race_aarakocra, + Race.GENASI to R.string.race_genasi, + Race.DEEP_GNOME to R.string.race_deep_gnome, + Race.GOLIATH to R.string.race_goliath, + Race.UNDETERMINED to R.string.race_undetermined, + ) + } +} \ 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 be7a62c..99cec13 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 @@ -12,6 +12,11 @@ import androidx.compose.runtime.remember import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat +import com.pixelized.rplexicon.ui.theme.colors.LexiconColors +import com.pixelized.rplexicon.ui.theme.colors.darkColorScheme +import com.pixelized.rplexicon.ui.theme.colors.lightColorScheme +import com.pixelized.rplexicon.ui.theme.shape.LexiconShapes +import com.pixelized.rplexicon.ui.theme.shape.lexiconShapes val LocalLexiconTheme = compositionLocalOf { error("LocalLexiconTheme not ready yet.") @@ -20,6 +25,7 @@ val LocalLexiconTheme = compositionLocalOf { @Stable data class LexiconTheme( val colorScheme: LexiconColors, + val shapes: LexiconShapes, ) @Composable @@ -32,7 +38,8 @@ fun LexiconTheme( colorScheme = when (darkTheme) { true -> darkColorScheme() else -> lightColorScheme() - } + }, + shapes = lexiconShapes() ) } @@ -40,10 +47,8 @@ fun LexiconTheme( if (!view.isInEditMode) { SideEffect { val window = (view.context as Activity).window - lexiconTheme.colorScheme.base.background.toArgb().let { - window.statusBarColor = it - window.navigationBarColor = it - } + window.statusBarColor = lexiconTheme.colorScheme.status.toArgb() + window.navigationBarColor = lexiconTheme.colorScheme.navigation.toArgb() WindowCompat.getInsetsController(window, view).let { it.isAppearanceLightStatusBars = !darkTheme it.isAppearanceLightNavigationBars = !darkTheme @@ -56,6 +61,7 @@ fun LexiconTheme( ) { MaterialTheme( colorScheme = lexiconTheme.colorScheme.base, + shapes = lexiconTheme.shapes.base, typography = Typography, content = content ) diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/theme/Color.kt b/app/src/main/java/com/pixelized/rplexicon/ui/theme/colors/LexiconColors.kt similarity index 75% rename from app/src/main/java/com/pixelized/rplexicon/ui/theme/Color.kt rename to app/src/main/java/com/pixelized/rplexicon/ui/theme/colors/LexiconColors.kt index fbb9210..4f37718 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/theme/Color.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/theme/colors/LexiconColors.kt @@ -1,4 +1,4 @@ -package com.pixelized.rplexicon.ui.theme +package com.pixelized.rplexicon.ui.theme.colors import androidx.compose.material3.ColorScheme import androidx.compose.material3.darkColorScheme @@ -6,13 +6,13 @@ import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.Color -import com.pixelized.rplexicon.ui.theme.colors.BaseDark -import com.pixelized.rplexicon.ui.theme.colors.BaseLight @Stable @Immutable class LexiconColors( val base: ColorScheme, + val status: Color, + val navigation: Color, val placeholder: Color, ) @@ -24,9 +24,13 @@ fun darkColorScheme( tertiary = BaseDark.Pink80, onPrimary = Color.White, ), + status: Color = Color.Transparent, + navigation: Color = Color.Transparent, placeholder: Color = Color(red = 49, green = 48, blue = 51), ) = LexiconColors( base = base, + status = status, + navigation = navigation, placeholder = placeholder, ) @@ -38,8 +42,12 @@ fun lightColorScheme( tertiary = BaseLight.Pink40, onPrimary = Color.White, ), + status: Color = Color.Transparent, + navigation: Color = Color.Transparent, placeholder: Color = Color(red = 230, green = 225, blue = 229), ) = LexiconColors( base = base, + status = status, + navigation = navigation, placeholder = placeholder, ) 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 new file mode 100644 index 0000000..a25ecc5 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/theme/shape/LexiconShapes.kt @@ -0,0 +1,21 @@ +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.Shape + +@Stable +class LexiconShapes( + val base: Shapes, + val textField: Shape +) + +@Stable +fun lexiconShapes( + base: Shapes = Shapes(), + textField: Shape = CircleShape, +) = LexiconShapes( + base = base, + textField = textField, +) \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/GlideHelp.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/GlideHelp.kt new file mode 100644 index 0000000..2ae29e1 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/GlideHelp.kt @@ -0,0 +1,40 @@ +package com.pixelized.rplexicon.utilitary + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalView + +@Stable +class GlideLoadingTransition( + target: MutableState, + alpha: State +) { + var target by target + val alpha by alpha +} + +@Composable +fun rememberLoadingTransition(model: () -> Any?): GlideLoadingTransition { + val isInEditMode = LocalView.current.isInEditMode + val key = model() + val target = remember(key) { + mutableStateOf(if (isInEditMode) 1f else 0f) + } + val alpha = animateFloatAsState( + targetValue = target.value, + label = "RememberLoadingTransition" + ) + return remember(key) { + GlideLoadingTransition( + target = target, + alpha = alpha, + ) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_arrow_drop_down_24.xml b/app/src/main/res/drawable/ic_arrow_down_24.xml similarity index 79% rename from app/src/main/res/drawable/ic_baseline_arrow_drop_down_24.xml rename to app/src/main/res/drawable/ic_arrow_down_24.xml index c1c897a..9e345b8 100644 --- a/app/src/main/res/drawable/ic_baseline_arrow_drop_down_24.xml +++ b/app/src/main/res/drawable/ic_arrow_down_24.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/res/drawable/ic_clear_24.xml b/app/src/main/res/drawable/ic_clear_24.xml new file mode 100644 index 0000000..844b6b6 --- /dev/null +++ b/app/src/main/res/drawable/ic_clear_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 60e24f5..8d94de1 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -28,8 +28,15 @@ Se connecter avec + Rechercher + Détails du personnage Description - History + Histoire Portrait + + Rechercher + Rechercher + Race + Sexe \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bd3828e..745a89a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -28,8 +28,15 @@ Sign in with + Search + Character\'s details Description - Histoire + History Portrait + + Search + Search + Race + Gender \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index bfa0207..53b5e00 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,5 +1,9 @@ - + +