Add basic UI element as POC.

This commit is contained in:
Andres Gomez, Thomas (ITDV CC) - AF (ext) 2023-07-13 22:55:59 +02:00
parent 1afd3bc02b
commit 6876ad7052
12 changed files with 606 additions and 9 deletions

View file

@ -58,7 +58,6 @@ android {
dependencies {
implementation("androidx.core:core-ktx:1.10.1")
// implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
implementation("androidx.activity:activity-compose:1.7.2")
// Compose
@ -66,9 +65,17 @@ dependencies {
implementation("androidx.compose.ui:ui-graphics:1.4.3")
implementation("androidx.compose.ui:ui-tooling-preview:1.4.3")
implementation("androidx.compose.material3:material3:1.1.1")
debugImplementation("androidx.compose.ui:ui-tooling:1.4.3")
// Hilt: Dependency injection
implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
implementation("com.google.dagger:hilt-android:2.45")
kapt("com.google.dagger:hilt-compiler:2.45")
// Image
implementation("com.github.skydoves:landscapist-glide:2.1.11")
kapt("com.github.bumptech.glide:compiler:4.14.2") // this have to be align with landscapist-glide
// Retrofit : Network
implementation("com.squareup.retrofit2:retrofit:2.9.0")
}

View file

@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".MainApplication"
android:allowBackup="true"

View file

@ -0,0 +1,17 @@
package com.pixelized.lexique.model
import android.net.Uri
data class Lexicon(
val name: String?,
val diminutive: String?,
val gender: Gender = Gender.UNDETERMINED,
val race: String?,
val portrait: List<Uri>,
val description: String?,
val history: String?,
) {
enum class Gender {
MALE, FEMALE, UNDETERMINED
}
}

View file

@ -0,0 +1,24 @@
package com.pixelized.lexique.module
import com.pixelized.lexique.network.IGoogleSpreadSheet
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retrofit
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class NetworkModule {
@Provides
@Singleton
fun provideGoogleSpreadSheet(): IGoogleSpreadSheet {
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl(IGoogleSpreadSheet.HOST)
.build()
return retrofit.create(IGoogleSpreadSheet::class.java)
}
}

View file

@ -0,0 +1,19 @@
package com.pixelized.lexique.network
import retrofit2.http.GET
interface IGoogleSpreadSheet {
@GET("spreadsheets/d/$ID/edit#gid=$LEXICON_GID")
fun getLexicon()
@GET("spreadsheets/d/$ID/edit#gid=$META_DATA_GID")
fun getMetaData()
companion object {
const val HOST = "https://docs.google.com/"
const val ID = "1oL9Nu5y37BPEbKxHre4TN9o8nrgy2JQoON4RRkdAHMs"
const val LEXICON_GID = 0
const val META_DATA_GID = "957635233"
}
}

View file

@ -0,0 +1,80 @@
package com.pixelized.lexique.repository
import android.net.Uri
import com.pixelized.lexique.model.Lexicon
import com.pixelized.lexique.network.IGoogleSpreadSheet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class LexiconRepository @Inject constructor(
private val spreadSheet: IGoogleSpreadSheet,
) {
private val scope = CoroutineScope(Dispatchers.IO)
private val _data = MutableStateFlow<List<Lexicon>>(emptyList())
val data: StateFlow<List<Lexicon>> get() = _data
init {
scope.launch {
_data.emit(sample())
}
}
private fun sample(): List<Lexicon> {
return listOf(
Lexicon(
name = "Brulkhai",
diminutive = "Bru",
gender = Lexicon.Gender.FEMALE,
race = "Demi-Orc",
portrait = listOf(
Uri.parse("https://drive.google.com/file/d/1a31xJ6DQnzqmGBndG-uo65HNQHPEUJnI/view?usp=sharing"),
),
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,
),
Lexicon(
name = "Léandre",
diminutive = null,
gender = Lexicon.Gender.MALE,
race = "Humain",
portrait = emptyList(),
description = null,
history = null,
),
Lexicon(
name = "Nelia",
diminutive = null,
gender = Lexicon.Gender.FEMALE,
race = "Elfe",
portrait = emptyList(),
description = null,
history = null,
),
Lexicon(
name = "Tigrane",
diminutive = null,
gender = Lexicon.Gender.MALE,
race = "Tieffelin",
portrait = emptyList(),
description = null,
history = null,
),
Lexicon(
name = "Unathana",
diminutive = "Una",
gender = Lexicon.Gender.FEMALE,
race = "Demi-Elfe",
portrait = emptyList(),
description = null,
history = null,
),
)
}
}

View file

@ -2,11 +2,69 @@ package com.pixelized.lexique.ui.screens.detail
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.net.Uri
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
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.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
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.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.lexique.ui.screens.lexicon.LexiconScreen
import com.pixelized.lexique.R
import com.pixelized.lexique.ui.theme.LexiconTheme
import com.skydoves.landscapist.ImageOptions
import com.skydoves.landscapist.glide.GlideImage
@Stable
data class CharacterDetailUio(
val name: String?,
val diminutive: String?,
val gender: String?,
val race: String?,
val portrait: List<Uri>,
val description: String?,
val history: String?,
)
@Composable
fun CharacterDetailScreen(
@ -15,9 +73,186 @@ fun CharacterDetailScreen(
}
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
@Composable
private fun CharacterDetailScreenContent() {
private fun CharacterDetailScreenContent(
modifier: Modifier = Modifier,
state: ScrollState = rememberScrollState(),
item: CharacterDetailUio,
onBack: () -> Unit,
) {
val colorScheme = MaterialTheme.colorScheme
val typography = MaterialTheme.typography
Scaffold(
modifier = modifier,
containerColor = Color.Transparent,
topBar = {
TopAppBar(
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
painter = painterResource(id = R.drawable.ic_arrow_back_ios_new_24),
contentDescription = null
)
}
},
title = { },
)
},
) { paddingValues ->
Box(
modifier = Modifier.padding(paddingValues = paddingValues),
) {
item.portrait.firstOrNull()?.let { uri ->
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(ratio = 1f)
.scrollOffset(scrollState = state) { -it / 2 },
) {
GlideImage(
modifier = Modifier.matchParentSize(),
imageModel = { uri.toString() },
imageOptions = ImageOptions(
alignment = Alignment.TopCenter,
contentScale = ContentScale.Crop,
colorFilter = remember {
ColorFilter.colorMatrix(
ColorMatrix().also { it.setToSaturation(0f) }
)
},
),
previewPlaceholder = R.drawable.ic_empty,
)
Box(
modifier = Modifier
.matchParentSize()
.background(brush = rememberBackgroundGradient())
)
}
}
Column(
modifier = Modifier
.verticalScroll(state)
.padding(top = 64.dp, bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
FlowRow(
modifier = Modifier.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
item.name?.let {
Text(
modifier = Modifier.alignByBaseline(),
style = typography.headlineSmall,
text = it,
)
}
item.diminutive?.let {
Text(
modifier = Modifier.alignByBaseline(),
style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) },
text = it,
)
}
}
Row(
modifier = Modifier.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
item.race?.let {
Text(
style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) },
text = it,
)
}
item.gender?.let {
Text(
style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) },
text = it,
)
}
}
item.description?.let {
Text(
modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp),
style = typography.titleMedium,
text = "Description",
)
Text(
modifier = Modifier.padding(horizontal = 16.dp),
style = remember {
typography.bodyMedium.copy(
shadow = Shadow(
color = colorScheme.surface.copy(alpha = 0.5f),
offset = Offset(x = 1f, y = 1f),
)
)
},
text = it,
)
}
item.history?.let {
Text(
modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp),
style = typography.titleMedium,
text = "Histoire",
)
Text(
modifier = Modifier.padding(horizontal = 16.dp),
style = typography.bodyMedium,
text = it,
)
}
if (item.portrait.isNotEmpty()) {
Text(
modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp),
style = typography.titleMedium,
text = "Portrait",
)
LazyRow(
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
items(items = item.portrait) {
GlideImage(
modifier = Modifier.height(320.dp),
imageModel = { it },
imageOptions = ImageOptions(
contentScale = ContentScale.FillHeight
),
previewPlaceholder = R.drawable.ic_empty,
)
}
}
}
}
}
}
}
@Composable
private fun rememberBackgroundGradient(): Brush {
val colorScheme = MaterialTheme.colorScheme
return remember {
Brush.verticalGradient(
colors = listOf(
colorScheme.surface.copy(alpha = 0.5f),
colorScheme.surface.copy(alpha = 1.0f),
)
)
}
}
@Stable
private fun Modifier.scrollOffset(
scrollState: ScrollState,
block: (Dp) -> Dp
): Modifier = composed {
val density = LocalDensity.current
this.offset(y = with(density) { block(scrollState.value.toDp()) })
}
@Composable
@ -25,6 +260,31 @@ private fun CharacterDetailScreenContent() {
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun CharacterDetailScreenContentPreview() {
LexiconTheme {
CharacterDetailScreenContent()
Surface {
CharacterDetailScreenContent(
modifier = Modifier.fillMaxSize(),
item = CharacterDetailUio(
name = "Brulkhai",
diminutive = "./ Bru",
gender = "female",
race = "Demi-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 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,
),
onBack = { },
)
}
}
}

View file

@ -0,0 +1,88 @@
package com.pixelized.lexique.ui.screens.lexicon
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.pixelized.lexique.ui.theme.LexiconTheme
@Stable
data class LexiconItemUio(
val name: String,
val diminutive: String?,
val gender: String?,
val race: String?,
)
@Composable
fun LexiconItem(
modifier: Modifier = Modifier,
item: LexiconItemUio,
) {
val typography = MaterialTheme.typography
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
modifier = Modifier.alignByBaseline(),
style = remember { typography.bodyLarge.copy(fontWeight = FontWeight.Bold) },
text = item.name,
)
Text(
modifier = Modifier.alignByBaseline(),
style = typography.labelMedium,
text = item.diminutive ?: ""
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) },
text = item.gender ?: ""
)
Text(
style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) },
text = item.race ?: ""
)
}
}
}
@Composable
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun LexiconItemContentPreview() {
LexiconTheme {
Surface {
LexiconItem(
modifier = Modifier.fillMaxWidth(),
item = LexiconItemUio(
name = "Brulkhai",
diminutive = "Bru",
gender = "f.",
race = "Demi-Orc",
)
)
}
}
}

View file

@ -2,8 +2,21 @@ package com.pixelized.lexique.ui.screens.lexicon
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.lexique.ui.theme.LexiconTheme
@ -11,12 +24,34 @@ import com.pixelized.lexique.ui.theme.LexiconTheme
fun LexiconScreen(
viewModel: LexiconViewModel = hiltViewModel(),
) {
Surface {
LexiconScreenContent(
items = viewModel.items,
onItem = { },
)
}
}
@Composable
private fun LexiconScreenContent() {
private fun LexiconScreenContent(
modifier: Modifier = Modifier,
items: State<List<LexiconItemUio>>,
onItem: (LexiconItemUio) -> Unit,
) {
LazyColumn(
modifier = modifier,
contentPadding = PaddingValues(vertical = 8.dp),
) {
items(items = items.value) {
LexiconItem(
modifier = Modifier
.clickable { onItem(it) }
.fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 16.dp),
item = it,
)
}
}
}
@Composable
@ -24,6 +59,23 @@ private fun LexiconScreenContent() {
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun LexiconScreenContentPreview() {
LexiconTheme {
LexiconScreenContent()
Surface {
LexiconScreenContent(
modifier = Modifier.fillMaxSize(),
items = remember {
mutableStateOf(
listOf(
LexiconItemUio(
name = "Brulkhai",
diminutive = "Bru",
gender = "f.",
race = "Demi-Orc",
)
)
)
},
onItem = { },
)
}
}
}

View file

@ -1,8 +1,42 @@
package com.pixelized.lexique.ui.screens.lexicon
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.lexique.model.Lexicon
import com.pixelized.lexique.repository.LexiconRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class LexiconViewModel @Inject constructor() : ViewModel()
class LexiconViewModel @Inject constructor(
private val repository: LexiconRepository,
) : ViewModel() {
// TODO : link it to a paginated DataSource
private val _items = mutableStateOf<List<LexiconItemUio>>(emptyList())
val items: State<List<LexiconItemUio>> get() = _items
init {
viewModelScope.launch {
repository.data.collect { items ->
_items.value = items.mapNotNull { item ->
item.name?.let {
LexiconItemUio(
name = item.name,
diminutive = item.diminutive?.let { "./ $it" },
gender = when (item.gender) {
Lexicon.Gender.MALE -> "m."
Lexicon.Gender.FEMALE -> "f."
Lexicon.Gender.UNDETERMINED -> "u."
},
race = item.race,
)
}
}
}
}
}
}

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="M17.77,3.77l-1.77,-1.77l-10,10l10,10l1.77,-1.77l-8.23,-8.23z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M0.0,0.0z" />
</vector>