Basic search implementation.

This commit is contained in:
Andres Gomez, Thomas (ITDV CC) - AF (ext) 2023-07-18 21:21:52 +02:00
parent dfc6c4e673
commit af5fb8f33c
21 changed files with 806 additions and 298 deletions

View file

@ -97,6 +97,9 @@ dependencies {
implementation("com.google.accompanist:accompanist-navigation-animation:0.30.1") implementation("com.google.accompanist:accompanist-navigation-animation:0.30.1")
implementation("com.google.accompanist:accompanist-placeholder: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 // Google service
implementation("com.google.android.gms:play-services-auth:20.6.0") implementation("com.google.android.gms:play-services-auth:20.6.0")
implementation( implementation(

View file

@ -9,19 +9,23 @@
<application <application
android:name=".MainApplication" android:name=".MainApplication"
android:allowBackup="true" android:allowBackup="true"
android:colorMode="wideColorGamut"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Lexique" android:theme="@style/Theme.Lexicon.NoActionBar"
tools:targetApi="31"> tools:targetApi="31">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/Theme.Lexique"> android:screenOrientation="portrait"
android:theme="@style/Theme.Lexicon.Starting"
android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -29,5 +33,4 @@
</intent-filter> </intent-filter>
</activity> </activity>
</application> </application>
</manifest> </manifest>

View file

@ -4,6 +4,7 @@ import android.app.Activity
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -15,13 +16,19 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier 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.navigation.ScreenNavHost
import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.ui.theme.LexiconTheme
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
val LocalActivity = staticCompositionLocalOf<Activity> { error("Activity not available") } val LocalActivity = staticCompositionLocalOf<Activity> {
val LocalSnack = error("Activity not available")
staticCompositionLocalOf<SnackbarHostState> { error("SnackbarHostState not available") } }
val LocalSnack = staticCompositionLocalOf<SnackbarHostState> {
error("SnackbarHostState not available")
}
val NO_WINDOW_INSETS = WindowInsets(0, 0, 0, 0)
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@ -29,6 +36,14 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// Disable system inset consuming.
WindowCompat.setDecorFitsSystemWindows(window, false)
// splashscreen management
installSplashScreen().apply {
setKeepOnScreenCondition { false }
}
setContent { setContent {
LexiconTheme { LexiconTheme {
CompositionLocalProvider( CompositionLocalProvider(
@ -36,6 +51,7 @@ class MainActivity : ComponentActivity() {
LocalSnack provides remember { SnackbarHostState() } LocalSnack provides remember { SnackbarHostState() }
) { ) {
Scaffold( Scaffold(
contentWindowInsets = NO_WINDOW_INSETS,
content = { padding -> content = { padding ->
Surface( Surface(
modifier = Modifier modifier = Modifier

View file

@ -5,15 +5,14 @@ import androidx.compose.animation.core.*
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
@ -25,24 +24,24 @@ import com.pixelized.rplexicon.ui.theme.LexiconTheme
@Composable @Composable
fun FloatingActionButton( 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, 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), icon: @Composable (RowScope.() -> Unit),
text: @Composable (RowScope.() -> Unit), text: @Composable (RowScope.() -> Unit),
) { ) {
LocalButton( OutlinedButton(
modifier = modifier, modifier = modifier,
onClick = onClick, onClick = onClick,
enabled = enabled, enabled = enabled,
@ -51,13 +50,16 @@ fun FloatingActionButton(
shape = shape, shape = shape,
colors = colors, colors = colors,
contentPadding = contentPadding, contentPadding = contentPadding,
border = border,
content = { content = {
FabContent( BoxWithConstraints {
expended = expended, FabContent(
innerSpacing = innerSpacing, expended = expended,
icon = icon, innerSpacing = innerSpacing,
text = text, icon = icon,
) text = text,
)
}
}, },
) )
} }
@ -69,9 +71,10 @@ private fun BoxWithConstraintsScope.FabContent(
icon: @Composable (RowScope.() -> Unit), icon: @Composable (RowScope.() -> Unit),
text: @Composable (RowScope.() -> Unit), text: @Composable (RowScope.() -> Unit),
) { ) {
val maxWidth = if (LocalView.current.isInEditMode) 300.dp else maxWidth
val width by animateDpAsState( val width by animateDpAsState(
label = "FabContentWidth", label = "FabContentWidth",
targetValue = if (expended) maxWidth else minWidth, targetValue = if (expended) maxWidth else 56.dp,
animationSpec = when (expended) { animationSpec = when (expended) {
true -> tween(durationMillis = 300, easing = FastOutSlowInEasing) true -> tween(durationMillis = 300, easing = FastOutSlowInEasing)
else -> tween(durationMillis = 300, delayMillis = 100, 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 @Composable
@Preview @Preview
private fun FloatingActionButtonPreview( private fun FloatingActionButtonPreview(
@ -162,6 +115,7 @@ private fun FloatingActionButtonPreview(
) { ) {
LexiconTheme { LexiconTheme {
FloatingActionButton( FloatingActionButton(
modifier = Modifier.padding(all = 16.dp),
expended = expended, expended = expended,
icon = { icon = {
Icon( Icon(

View file

@ -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<T>(
@StringRes val label: Int,
val values: List<Pair<T, Int>>,
val value: State<Pair<T?, String>>,
val onValueChange: (T?, String) -> Unit,
) {
companion object {
fun <T> 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 <T> DropDownField(
modifier: Modifier = Modifier,
field: DropDownFieldUio<T>,
) {
var expended by remember(field) { mutableStateOf(false) }
ExposedDropdownMenuBox(
modifier = modifier,
expanded = expended,
onExpandedChange = { expended = !expended && field.value.value.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<String?, String>,
) {
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<Pair<String?, String>> {
override val values: Sequence<Pair<String?, String>> = sequenceOf(null to "", "" to "preview")
}

View file

@ -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<String>,
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<String> {
override val values: Sequence<String> = sequenceOf("", "preview")
}

View file

@ -5,9 +5,9 @@ import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.animation.core.Spring import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring import androidx.compose.animation.core.spring
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -16,13 +16,13 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.width 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.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -71,9 +71,12 @@ fun AuthenticationScreen(
val state = authenticationVM.rememberAuthenticationState() val state = authenticationVM.rememberAuthenticationState()
Surface { Surface {
PartyBackground()
AuthenticationScreenContent( AuthenticationScreenContent(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.systemBarsPadding()
.padding(all = 16.dp), .padding(all = 16.dp),
version = versionVM.version, version = versionVM.version,
onGoogleSignIn = { onGoogleSignIn = {
@ -108,22 +111,19 @@ private fun AuthenticationScreenContent(
) { ) {
val typography = MaterialTheme.typography val typography = MaterialTheme.typography
PartyBackground()
Column( Column(
modifier = modifier, modifier = modifier,
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Bottom), verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Bottom),
horizontalAlignment = Alignment.End, horizontalAlignment = Alignment.End,
) { ) {
Button( OutlinedButton(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.border( .height(56.dp),
width = 2.dp, border = BorderStroke(
color = MaterialTheme.colorScheme.primary, width = 2.dp,
shape = CircleShape, color = MaterialTheme.colorScheme.primary,
), ),
colors = ButtonDefaults.outlinedButtonColors(),
onClick = onGoogleSignIn, onClick = onGoogleSignIn,
) { ) {
Text(text = rememeberGoogleStringResource()) Text(text = rememeberGoogleStringResource())

View file

@ -6,7 +6,6 @@ import android.net.Uri
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.ScrollState import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -17,9 +16,9 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
@ -40,6 +39,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.composed import androidx.compose.ui.composed
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color 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.ColorMatrix
import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource 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.R
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.rememberLoadingTransition
import com.skydoves.landscapist.ImageOptions import com.skydoves.landscapist.ImageOptions
import com.skydoves.landscapist.glide.GlideImage import com.skydoves.landscapist.glide.GlideImage
import com.skydoves.landscapist.glide.GlideImageState
@Stable @Stable
data class CharacterDetailUio( data class CharacterDetailUio(
@ -83,7 +86,6 @@ fun CharacterDetailScreen(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
item = viewModel.character, item = viewModel.character,
onBack = { screen.popBackStack() }, onBack = { screen.popBackStack() },
onImage = { },
) )
} }
} }
@ -95,7 +97,6 @@ private fun CharacterDetailScreenContent(
state: ScrollState = rememberScrollState(), state: ScrollState = rememberScrollState(),
item: State<CharacterDetailUio>, item: State<CharacterDetailUio>,
onBack: () -> Unit, onBack: () -> Unit,
onImage: (Uri) -> Unit,
) { ) {
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val typography = MaterialTheme.typography val typography = MaterialTheme.typography
@ -129,9 +130,16 @@ private fun CharacterDetailScreenContent(
.aspectRatio(ratio = 1f) .aspectRatio(ratio = 1f)
.scrollOffset(scrollState = state) { -it / 2 }, .scrollOffset(scrollState = state) { -it / 2 },
) { ) {
val transition = rememberLoadingTransition { uri }
GlideImage( GlideImage(
modifier = Modifier.matchParentSize(), modifier = Modifier
imageModel = { uri.toString() }, .matchParentSize()
.alpha(alpha = transition.alpha),
onImageStateChanged = {
if (it is GlideImageState.Success) {
transition.target = 1f
}
},
imageOptions = ImageOptions( imageOptions = ImageOptions(
alignment = Alignment.TopCenter, alignment = Alignment.TopCenter,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
@ -141,7 +149,8 @@ private fun CharacterDetailScreenContent(
) )
}, },
), ),
previewPlaceholder = R.drawable.ic_empty, imageModel = { uri.toString() },
previewPlaceholder = R.drawable.im_brulkhai,
) )
Box( Box(
modifier = Modifier modifier = Modifier
@ -220,6 +229,8 @@ private fun CharacterDetailScreenContent(
) )
} }
if (item.value.portrait.isNotEmpty()) { if (item.value.portrait.isNotEmpty()) {
val configuration = LocalConfiguration.current
val maxSize = remember { (configuration.screenWidthDp.dp - 16.dp * 2) }
Text( Text(
modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp), modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp),
style = typography.titleMedium, style = typography.titleMedium,
@ -230,15 +241,21 @@ private fun CharacterDetailScreenContent(
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
items(items = item.value.portrait) { items(items = item.value.portrait) {
val transition = rememberLoadingTransition { it }
GlideImage( GlideImage(
modifier = Modifier modifier = Modifier
.clickable { onImage(it) } .sizeIn(maxWidth = maxSize, maxHeight = maxSize)
.height(320.dp), .alpha(alpha = transition.alpha),
imageModel = { it }, onImageStateChanged = {
if (it is GlideImageState.Success) {
transition.target = 1f
}
},
imageOptions = ImageOptions( imageOptions = ImageOptions(
contentScale = ContentScale.FillHeight 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, race = R.string.race_half_orc,
portrait = listOf( 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/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 lorsquelle est en bonne santé. Elle a les cheveux châtains, les yeux noisettes et la peau couleur gris-vert typique de son espèce. Dun tempérament taciturne, elle parle peu et de façon concise. Elle est parfois brutale, aussi bien physiquement que verbalement, Elle ne prend cependant aucun plaisir à malmener ceux quelle considère plus faibles quelle. Dune nature simple et honnête, elle ne mâche pas ses mots et ne dissimule généralement pas ses pensées. Son intelligence modeste est plus le reflet dun manque déducation et dune capacité limitée à gérer ses émotions quà une débilité congénitale. Elle voue à la force un culte car cest par son expression quelle se sent vraiment vivante et éprouve de grandes difficultés vis à vis de ceux quelle nomme foshnu (bébé, chouineur en commun).", description = "Brulkhai, ou plus simplement Bru, est solidement bâti. Elle mesure 192 cm pour 110 kg de muscles lorsquelle est en bonne santé. Elle a les cheveux châtains, les yeux noisettes et la peau couleur gris-vert typique de son espèce. Dun tempérament taciturne, elle parle peu et de façon concise. Elle est parfois brutale, aussi bien physiquement que verbalement, Elle ne prend cependant aucun plaisir à malmener ceux quelle considère plus faibles quelle. Dune nature simple et honnête, elle ne mâche pas ses mots et ne dissimule généralement pas ses pensées. Son intelligence modeste est plus le reflet dun manque déducation et dune capacité limitée à gérer ses émotions quà une débilité congénitale. Elle voue à la force un culte car cest par son expression quelle se sent vraiment vivante et éprouve de grandes difficultés vis à vis de ceux quelle nomme foshnu (bébé, chouineur en commun).",
history = null, history = null,
@ -304,7 +312,6 @@ private fun CharacterDetailScreenContentPreview() {
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
item = character, item = character,
onBack = { }, onBack = { },
onImage = { },
) )
} }
} }

View file

@ -55,49 +55,53 @@ fun LexiconItem(
) { ) {
val typography = MaterialTheme.typography val typography = MaterialTheme.typography
Column( Surface(modifier = modifier) {
modifier = modifier, Column(
verticalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier
) { .fillMaxWidth()
FlowRow( .padding(vertical = 8.dp, horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp), verticalArrangement = Arrangement.spacedBy(4.dp),
) { ) {
Text( FlowRow(
modifier = Modifier horizontalArrangement = Arrangement.spacedBy(4.dp),
.alignByBaseline() ) {
.placeholder { item.placeholder }, Text(
style = remember { typography.bodyLarge.copy(fontWeight = FontWeight.Bold) }, modifier = Modifier
maxLines = 1, .alignByBaseline()
text = item.name, .placeholder { item.placeholder },
) style = remember { typography.bodyLarge.copy(fontWeight = FontWeight.Bold) },
Text( maxLines = 1,
modifier = Modifier text = item.name,
.alignByBaseline() )
.placeholder { item.placeholder }, Text(
style = typography.labelMedium, modifier = Modifier
maxLines = 1, .alignByBaseline()
text = item.diminutive ?: "" .placeholder { item.placeholder },
) style = typography.labelMedium,
} maxLines = 1,
Row( text = item.diminutive ?: ""
horizontalArrangement = Arrangement.spacedBy(4.dp), )
) { }
Text( Row(
modifier = Modifier horizontalArrangement = Arrangement.spacedBy(4.dp),
.alignByBaseline() ) {
.placeholder { item.placeholder }, Text(
style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) }, modifier = Modifier
maxLines = 1, .alignByBaseline()
text = stringResource(id = item.gender) .placeholder { item.placeholder },
) style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) },
Text( maxLines = 1,
modifier = Modifier text = stringResource(id = item.gender)
.alignByBaseline() )
.placeholder { item.placeholder }, Text(
style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) }, modifier = Modifier
maxLines = 1, .alignByBaseline()
text = stringResource(id = item.race) .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 { LexiconTheme {
Surface { Surface {
LexiconItem( LexiconItem(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 16.dp),
item = LexiconItemUio( item = LexiconItemUio(
id = 0, id = 0,
name = "Brulkhai", name = "Brulkhai",

View file

@ -7,6 +7,7 @@ import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut 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.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items 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.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -46,12 +50,15 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.LocalSnack import com.pixelized.rplexicon.LocalSnack
import com.pixelized.rplexicon.NO_WINDOW_INSETS
import com.pixelized.rplexicon.R 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.LocalScreenNavHost
import com.pixelized.rplexicon.ui.navigation.screens.navigateToCharacterDetail import com.pixelized.rplexicon.ui.navigation.screens.navigateToCharacterDetail
import com.pixelized.rplexicon.ui.navigation.screens.navigateToSearch import com.pixelized.rplexicon.ui.navigation.screens.navigateToSearch
@ -100,6 +107,7 @@ fun LexiconScreen(
Surface { Surface {
LexiconScreenContent( LexiconScreenContent(
modifier = Modifier.systemBarsPadding(),
items = viewModel.items, items = viewModel.items,
lazyColumnState = lazyListState, lazyColumnState = lazyListState,
refreshState = refresh, refreshState = refresh,
@ -139,35 +147,44 @@ private fun LexiconScreenContent(
) { ) {
Scaffold( Scaffold(
modifier = modifier, modifier = modifier,
contentWindowInsets = NO_WINDOW_INSETS,
topBar = { topBar = {
TopAppBar( TopAppBar(
windowInsets = NO_WINDOW_INSETS,
title = { title = {
Text(text = stringResource(id = R.string.app_name)) Text(text = stringResource(id = R.string.app_name))
}, },
) )
}, },
// floatingActionButton = { floatingActionButton = {
// FloatingActionButton( Box(
// modifier = Modifier.padding(start = 32.dp), modifier = Modifier
// expended = isFabExpended.value, .fillMaxWidth()
// onClick = onSearch, .height(56.dp)
// icon = { .padding(start = 32.dp), // `Fix` Scaffold content size for FAB.
// Icon( contentAlignment = Alignment.CenterEnd,
// tint = MaterialTheme.colorScheme.onPrimary, ) {
// painter = painterResource(id = R.drawable.ic_baseline_search_24), AnimatedVisibility(
// contentDescription = null, visible = items.value.isNotEmpty(),
// ) enter = fadeIn(),
// }, exit = fadeOut(),
// text = { ) {
// val typography = MaterialTheme.typography FloatingActionButton(
// Text( expended = isFabExpended.value,
// color = MaterialTheme.colorScheme.onPrimary, onClick = onSearch,
// style = remember { typography.bodyLarge.copy(fontWeight = FontWeight.Bold) }, icon = {
// text = "Rechercher", Icon(
// ) painter = painterResource(id = R.drawable.ic_baseline_search_24),
// }, contentDescription = null,
// ) )
// } },
text = {
Text(text = stringResource(id = R.string.lexicon_search))
},
)
}
}
}
) { padding -> ) { padding ->
Box( Box(
modifier = Modifier.padding(paddingValues = padding), modifier = Modifier.padding(paddingValues = padding),
@ -186,15 +203,12 @@ private fun LexiconScreenContent(
state = lazyColumnState, state = lazyColumnState,
contentPadding = PaddingValues( contentPadding = PaddingValues(
top = 8.dp, top = 8.dp,
bottom = 8.dp + 16.dp // + 56.dp + 16.dp, bottom = 8.dp + 16.dp + 56.dp + 16.dp,
), ),
) { ) {
items(count = 6) { items(count = 6) {
LexiconItem( LexiconItem(
modifier = Modifier modifier = Modifier.animateItemPlacement(),
.animateItemPlacement()
.fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 16.dp),
item = LexiconItemUio.Brulkhai, item = LexiconItemUio.Brulkhai,
) )
} }
@ -207,7 +221,7 @@ private fun LexiconScreenContent(
state = lazyColumnState, state = lazyColumnState,
contentPadding = PaddingValues( contentPadding = PaddingValues(
top = 8.dp, top = 8.dp,
bottom = 8.dp + 16.dp // + 56.dp + 16.dp, bottom = 8.dp + 16.dp + 56.dp + 16.dp,
), ),
) { ) {
items( items(
@ -218,9 +232,7 @@ private fun LexiconScreenContent(
LexiconItem( LexiconItem(
modifier = Modifier modifier = Modifier
.animateItemPlacement() .animateItemPlacement()
.clickable { onItem(it) } .clickable { onItem(it) },
.fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 16.dp),
item = it, item = it,
) )
} }

View file

@ -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_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.DropdownMenuItem import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.ExposedDropdownMenuBox 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.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource 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.Preview
import androidx.compose.ui.unit.dp 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.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.LocalScreenNavHost
import com.pixelized.rplexicon.ui.navigation.screens.navigateToCharacterDetail
import com.pixelized.rplexicon.ui.screens.lexicon.LexiconItem
import com.pixelized.rplexicon.ui.screens.lexicon.LexiconItemUio
import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.ui.theme.LexiconTheme
@Stable
data class SearchFormUio(
val search: TextFieldUio,
val gender: DropDownFieldUio<Lexicon.Gender>,
val race: DropDownFieldUio<Lexicon.Race>,
)
@Composable @Composable
fun SearchScreen() { fun SearchScreen(
viewModel: SearchViewModel = hiltViewModel(),
) {
val screen = LocalScreenNavHost.current val screen = LocalScreenNavHost.current
Surface { Surface {
SearchScreenContent( SearchScreenContent(
modifier = Modifier.fillMaxSize(), modifier = Modifier
.fillMaxSize()
.systemBarsPadding()
.imePadding(),
items = viewModel.filter,
form = viewModel.form,
onItem = {
screen.navigateToCharacterDetail(id = it.id)
},
onBack = { onBack = {
screen.popBackStack() screen.popBackStack()
} }
@ -49,17 +82,23 @@ fun SearchScreen() {
} }
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable @Composable
private fun SearchScreenContent( private fun SearchScreenContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
lazyColumnState: LazyListState = rememberLazyListState(),
items: State<List<LexiconItemUio>>,
form: SearchFormUio,
onBack: () -> Unit, onBack: () -> Unit,
onItem: (LexiconItemUio) -> Unit,
) { ) {
Scaffold( Scaffold(
modifier = modifier, modifier = modifier,
contentWindowInsets = NO_WINDOW_INSETS,
containerColor = Color.Transparent, containerColor = Color.Transparent,
topBar = { topBar = {
TopAppBar( TopAppBar(
windowInsets = NO_WINDOW_INSETS,
navigationIcon = { navigationIcon = {
IconButton(onClick = onBack) { IconButton(onClick = onBack) {
Icon( Icon(
@ -69,99 +108,52 @@ private fun SearchScreenContent(
} }
}, },
title = { title = {
Text(text = "Rechercher") Text(text = stringResource(id = R.string.search_field_title))
}, },
) )
}, },
) { ) { paddingValues ->
Column( LazyColumn(
modifier = Modifier modifier = Modifier
.padding(paddingValues = it) .fillMaxSize()
.padding(all = 16.dp), .padding(paddingValues = paddingValues),
verticalArrangement = Arrangement.spacedBy(8.dp), state = lazyColumnState,
contentPadding = PaddingValues(vertical = 8.dp),
) { ) {
TextField( item {
modifier = Modifier.fillMaxWidth(), Column(
value = "", modifier = Modifier.padding(horizontal = 16.dp),
label = { verticalArrangement = Arrangement.spacedBy(2.dp),
Text("Nom") ) {
}, TextField(
onValueChange = { _ -> }, modifier = Modifier.fillMaxWidth(),
colors = TextFieldDefaults.colors( field = form.search,
focusedContainerColor = MaterialTheme.colorScheme.surface, )
unfocusedContainerColor = MaterialTheme.colorScheme.surface, Row(
), horizontalArrangement = Arrangement.spacedBy(8.dp),
) ) {
Row( DropDownField(
horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.weight(1f),
) { field = form.gender,
DropDownField( )
modifier = Modifier.weight(1f), DropDownField(
subject = remember { mutableStateOf("1") }, modifier = Modifier.weight(1f),
subjects = listOf("1", "2"), field = form.race,
onChange = { }, )
expanded = remember { mutableStateOf(false) }, }
onExpandedChange = { } Divider(modifier = Modifier.padding(top = 16.dp, bottom = 8.dp))
) }
DropDownField(
modifier = Modifier.weight(1f),
subject = remember { mutableStateOf("1") },
subjects = listOf("1", "2"),
onChange = { },
expanded = remember { mutableStateOf(false) },
onExpandedChange = { }
)
} }
} items(
} items = items.value,
} key = { it.id },
contentType = { "Lexicon" },
@OptIn(ExperimentalMaterialApi::class) ) {
@Composable LexiconItem(
fun DropDownField( modifier = Modifier
modifier: Modifier = Modifier, .animateItemPlacement()
subjects: List<String>, .clickable { onItem(it) },
subject: State<String>, item = it,
onChange: (String) -> Unit,
expanded: State<Boolean>,
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) },
) )
} }
} }
@ -173,9 +165,37 @@ fun DropDownField(
@Preview(uiMode = UI_MODE_NIGHT_YES) @Preview(uiMode = UI_MODE_NIGHT_YES)
private fun SearchScreenContentPreview() { private fun SearchScreenContentPreview() {
LexiconTheme { LexiconTheme {
val context = LocalContext.current
Surface { Surface {
SearchScreenContent( SearchScreenContent(
modifier = Modifier.fillMaxSize(), 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 = { }, onBack = { },
) )
} }

View file

@ -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<Pair<Gender?, String>>(null to "")
private val race = mutableStateOf<Pair<Race?, String>>(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<Lexicon> = 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,
)
}
}

View file

@ -12,6 +12,11 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat 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<LexiconTheme> { val LocalLexiconTheme = compositionLocalOf<LexiconTheme> {
error("LocalLexiconTheme not ready yet.") error("LocalLexiconTheme not ready yet.")
@ -20,6 +25,7 @@ val LocalLexiconTheme = compositionLocalOf<LexiconTheme> {
@Stable @Stable
data class LexiconTheme( data class LexiconTheme(
val colorScheme: LexiconColors, val colorScheme: LexiconColors,
val shapes: LexiconShapes,
) )
@Composable @Composable
@ -32,7 +38,8 @@ fun LexiconTheme(
colorScheme = when (darkTheme) { colorScheme = when (darkTheme) {
true -> darkColorScheme() true -> darkColorScheme()
else -> lightColorScheme() else -> lightColorScheme()
} },
shapes = lexiconShapes()
) )
} }
@ -40,10 +47,8 @@ fun LexiconTheme(
if (!view.isInEditMode) { if (!view.isInEditMode) {
SideEffect { SideEffect {
val window = (view.context as Activity).window val window = (view.context as Activity).window
lexiconTheme.colorScheme.base.background.toArgb().let { window.statusBarColor = lexiconTheme.colorScheme.status.toArgb()
window.statusBarColor = it window.navigationBarColor = lexiconTheme.colorScheme.navigation.toArgb()
window.navigationBarColor = it
}
WindowCompat.getInsetsController(window, view).let { WindowCompat.getInsetsController(window, view).let {
it.isAppearanceLightStatusBars = !darkTheme it.isAppearanceLightStatusBars = !darkTheme
it.isAppearanceLightNavigationBars = !darkTheme it.isAppearanceLightNavigationBars = !darkTheme
@ -56,6 +61,7 @@ fun LexiconTheme(
) { ) {
MaterialTheme( MaterialTheme(
colorScheme = lexiconTheme.colorScheme.base, colorScheme = lexiconTheme.colorScheme.base,
shapes = lexiconTheme.shapes.base,
typography = Typography, typography = Typography,
content = content content = content
) )

View file

@ -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.ColorScheme
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
@ -6,13 +6,13 @@ import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import com.pixelized.rplexicon.ui.theme.colors.BaseDark
import com.pixelized.rplexicon.ui.theme.colors.BaseLight
@Stable @Stable
@Immutable @Immutable
class LexiconColors( class LexiconColors(
val base: ColorScheme, val base: ColorScheme,
val status: Color,
val navigation: Color,
val placeholder: Color, val placeholder: Color,
) )
@ -24,9 +24,13 @@ fun darkColorScheme(
tertiary = BaseDark.Pink80, tertiary = BaseDark.Pink80,
onPrimary = Color.White, onPrimary = Color.White,
), ),
status: Color = Color.Transparent,
navigation: Color = Color.Transparent,
placeholder: Color = Color(red = 49, green = 48, blue = 51), placeholder: Color = Color(red = 49, green = 48, blue = 51),
) = LexiconColors( ) = LexiconColors(
base = base, base = base,
status = status,
navigation = navigation,
placeholder = placeholder, placeholder = placeholder,
) )
@ -38,8 +42,12 @@ fun lightColorScheme(
tertiary = BaseLight.Pink40, tertiary = BaseLight.Pink40,
onPrimary = Color.White, onPrimary = Color.White,
), ),
status: Color = Color.Transparent,
navigation: Color = Color.Transparent,
placeholder: Color = Color(red = 230, green = 225, blue = 229), placeholder: Color = Color(red = 230, green = 225, blue = 229),
) = LexiconColors( ) = LexiconColors(
base = base, base = base,
status = status,
navigation = navigation,
placeholder = placeholder, placeholder = placeholder,
) )

View file

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

View file

@ -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<Float>,
alpha: State<Float>
) {
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,
)
}
}

View file

@ -1,5 +1,5 @@
<vector android:height="24dp" android:tint="#000000" <vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24" android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M7,10l5,5 5,-5z"/> <path android:fillColor="@android:color/white" android:pathData="M7.41,8.59L12,13.17l4.59,-4.58L18,10l-6,6 -6,-6 1.41,-1.41z"/>
</vector> </vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

View file

@ -28,8 +28,15 @@
<string name="action_google_sign_in">Se connecter avec</string> <string name="action_google_sign_in">Se connecter avec</string>
<string name="lexicon_search">Rechercher</string>
<string name="detail_title">Détails du personnage</string> <string name="detail_title">Détails du personnage</string>
<string name="detail_description">Description</string> <string name="detail_description">Description</string>
<string name="detail_history">History</string> <string name="detail_history">Histoire</string>
<string name="detail_portrait">Portrait</string> <string name="detail_portrait">Portrait</string>
<string name="search_field_title">Rechercher</string>
<string name="search_field_search">Rechercher</string>
<string name="search_field_race">Race</string>
<string name="search_field_gender">Sexe</string>
</resources> </resources>

View file

@ -28,8 +28,15 @@
<string name="action_google_sign_in">Sign in with</string> <string name="action_google_sign_in">Sign in with</string>
<string name="lexicon_search">Search</string>
<string name="detail_title">Character\'s details</string> <string name="detail_title">Character\'s details</string>
<string name="detail_description">Description</string> <string name="detail_description">Description</string>
<string name="detail_history">Histoire</string> <string name="detail_history">History</string>
<string name="detail_portrait">Portrait</string> <string name="detail_portrait">Portrait</string>
<string name="search_field_title">Search</string>
<string name="search_field_search">Search</string>
<string name="search_field_race">Race</string>
<string name="search_field_gender">Gender</string>
</resources> </resources>

View file

@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.Lexique" parent="android:Theme.Material.Light.NoActionBar" /> <style name="Theme.Lexicon.Starting" parent="Theme.SplashScreen">
<item name="postSplashScreenTheme">@style/Theme.Lexicon.NoActionBar</item>
</style>
<style name="Theme.Lexicon.NoActionBar" parent="android:Theme.Material.Light.NoActionBar" />
</resources> </resources>