Change the parsing mechanist to allow more flexibility.

This commit is contained in:
Andres Gomez, Thomas (ITDV CC) - AF (ext) 2023-07-17 10:56:38 +02:00
parent f5c10c5154
commit fb31de8130
19 changed files with 577 additions and 140 deletions

View file

@ -24,8 +24,8 @@ android {
applicationId = "com.pixelized.rplexicon"
minSdk = 26
targetSdk = 33
versionCode = 1
versionName = "0.1.0"
versionCode = 2
versionName = "0.1.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View file

@ -11,8 +11,8 @@
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 1,
"versionName": "0.1.0",
"versionCode": 2,
"versionName": "0.1.1",
"outputFile": "app-release.apk"
}
],

View file

@ -6,13 +6,32 @@ data class Lexicon(
val id: Int,
val name: String,
val diminutive: String?,
val gender: Gender = Gender.UNDETERMINED,
val race: String?,
val gender: Gender,
val race: Race,
val portrait: List<Uri>,
val description: String?,
val history: String?,
) {
enum class Gender {
MALE, FEMALE, UNDETERMINED
MALE,
FEMALE,
UNDETERMINED
}
enum class Race {
ELF,
HALFLING,
HUMAN,
DWARF,
HALF_ELF,
HALF_ORC,
DRAGONBORN,
GNOME,
TIEFLING,
AARAKOCRA,
GENASI,
DEEP_GNOME,
GOLIATH,
UNDETERMINED
}
}

View file

@ -27,7 +27,7 @@ class AuthenticationRepository @Inject constructor(
.setBackOff(ExponentialBackOff())
credential.selectedAccount = signInCredential.value?.let {
Account(it.id, "google")
Account(it.id, ACCOUNT_TYPE)
}
return credential
@ -40,8 +40,8 @@ class AuthenticationRepository @Inject constructor(
}
companion object {
private const val ACCOUNT_TYPE = "google"
private val capabilities = listOf(
// SheetsScopes.SPREADSHEETS,
SheetsScopes.SPREADSHEETS_READONLY,
)
}

View file

@ -28,7 +28,6 @@ class LexiconRepository @Inject constructor(
GsonFactory(),
authenticationRepository.credential,
)
.setApplicationName("RP-Lexique")
.build()
else -> null
@ -45,7 +44,7 @@ class LexiconRepository @Inject constructor(
throw ServiceNotReady()
} else {
withContext(Dispatchers.IO) {
val request = service.spreadsheets().values().get(ID, LEXIQUE)
val request = service.spreadsheets().values().get(Sheet.ID, Sheet.LEXIQUE)
val data = request.execute()
updateData(data = data)
}
@ -55,53 +54,98 @@ class LexiconRepository @Inject constructor(
@Throws(IncompatibleSheetStructure::class)
private fun updateData(data: ValueRange?) {
val sheet = data?.values?.sheet()
var sheetStructure: Map<String, Int>? = null
val lexicon: List<Lexicon> = sheet?.mapIndexedNotNull { index, row ->
if (index == 0) {
checkSheetStructure(firstRow = row)
null
} else {
parseCharacterRow(index = index - 1, row = row as? List<Any>?)
when {
index == 0 -> {
sheetStructure = checkSheetStructure(firstRow = row)
null
}
row is List<*> -> parseCharacterRow(
sheetStructure = sheetStructure,
index = index - 1,
row = row,
)
else -> null
}
} ?: emptyList()
_data.tryEmit(lexicon)
}
@Throws(IncompatibleSheetStructure::class)
private fun checkSheetStructure(firstRow: Any?) {
when {
firstRow !is ArrayList<*> -> throw IncompatibleSheetStructure("First row is not a List: $firstRow")
firstRow.size < 7 -> throw IncompatibleSheetStructure("First row have not enough column: ${firstRow.size}, $firstRow")
firstRow.size > 7 -> throw IncompatibleSheetStructure("First row have too mush columns: ${firstRow.size}, $firstRow")
else -> {
for (index in 0..6) {
if (columns[index] != firstRow[index]) {
throw IncompatibleSheetStructure("Column at index:$index should be ${columns[index]} but was ${firstRow[index]}")
}
}
private fun checkSheetStructure(firstRow: Any?): HashMap<String, Int> {
// check if the row is a list
if (firstRow !is ArrayList<*>) {
throw IncompatibleSheetStructure("First row is not a List: $firstRow")
}
// parse the first line to find element that we recognize.
val sheetStructure = hashMapOf<String, Int>()
firstRow.forEachIndexed { index, cell ->
when (cell as? String) {
Sheet.NAME,
Sheet.DIMINUTIVE,
Sheet.GENDER,
Sheet.RACE,
Sheet.PORTRAIT,
Sheet.DESCRIPTION,
Sheet.HISTORY -> sheetStructure[cell] = index
}
}
// check if we found everything we need.
when {
sheetStructure.size < Sheet.KNOWN_COLUMN -> throw IncompatibleSheetStructure(
message = "Sheet header row does not have enough column: ${firstRow.size}.\nstructure: $firstRow\nheader: $sheetStructure"
)
sheetStructure.size > Sheet.KNOWN_COLUMN -> throw IncompatibleSheetStructure(
message = "Sheet header row does have too mush columns: ${firstRow.size}.\nstructure: $firstRow\nheader: $sheetStructure"
)
}
return sheetStructure
}
private fun parseCharacterRow(index: Int, row: List<Any>?): Lexicon? {
val name = row?.getOrNull(0) as? String
val diminutive = row?.getOrNull(1) as? String?
val gender = row?.getOrNull(2) as? String?
val race = row?.getOrNull(3) as? String?
val portrait = row?.getOrNull(4) as? String?
val description = row?.getOrNull(5) as? String?
val history = row?.getOrNull(6) as? String?
private fun parseCharacterRow(
sheetStructure: Map<String, Int>?,
index: Int,
row: List<*>?,
): Lexicon? {
val name = row?.getOrNull(sheetStructure.name) as? String
val diminutive = row?.getOrNull(sheetStructure.diminutive) as? String?
val gender = row?.getOrNull(sheetStructure.gender) as? String?
val race = row?.getOrNull(sheetStructure.race) as? String?
val portrait = row?.getOrNull(sheetStructure.portrait) as? String?
val description = row?.getOrNull(sheetStructure.description) as? String?
val history = row?.getOrNull(sheetStructure.history) as? String?
return if (name != null) {
Lexicon(
id = index,
name = name,
diminutive = diminutive?.takeIf { it.isNotBlank() },
gender = when (gender) {
"Male" -> Lexicon.Gender.MALE
"Femelle" -> Lexicon.Gender.FEMALE
gender = when (gender?.takeIf { it.isNotBlank() }) {
Gender.MALE -> Lexicon.Gender.MALE
Gender.FEMALE -> Lexicon.Gender.FEMALE
else -> Lexicon.Gender.UNDETERMINED
},
race = race?.takeIf { it.isNotBlank() },
race = when (race?.takeIf { it.isNotBlank() }) {
Race.ELF -> Lexicon.Race.ELF
Race.HALFLING -> Lexicon.Race.HALFLING
Race.HUMAN -> Lexicon.Race.HUMAN
Race.DWARF -> Lexicon.Race.DWARF
Race.HALF_ELF -> Lexicon.Race.HALF_ELF
Race.HALF_ORC -> Lexicon.Race.HALF_ORC
Race.DRAGONBORN -> Lexicon.Race.DRAGONBORN
Race.GNOME -> Lexicon.Race.GNOME
Race.TIEFLING -> Lexicon.Race.TIEFLING
Race.AARAKOCRA -> Lexicon.Race.AARAKOCRA
Race.GENASI -> Lexicon.Race.GENASI
Race.DEEP_GNOME -> Lexicon.Race.DEEP_GNOME
Race.GOLIATH -> Lexicon.Race.GOLIATH
else -> Lexicon.Race.UNDETERMINED
},
portrait = portrait?.split("\n")?.mapNotNull { it.toUriOrNull() } ?: emptyList(),
description = description?.takeIf { it.isNotBlank() },
history = history?.takeIf { it.isNotBlank() },
@ -124,16 +168,53 @@ class LexiconRepository @Inject constructor(
null
}
private val Map<String, Int>?.name: Int get() = this?.getValue(Sheet.NAME) ?: 0
private val Map<String, Int>?.diminutive: Int get() = this?.getValue(Sheet.DIMINUTIVE) ?: 1
private val Map<String, Int>?.gender: Int get() = this?.getValue(Sheet.GENDER) ?: 2
private val Map<String, Int>?.race: Int get() = this?.getValue(Sheet.RACE) ?: 3
private val Map<String, Int>?.portrait: Int get() = this?.getValue(Sheet.PORTRAIT) ?: 4
private val Map<String, Int>?.description: Int get() = this?.getValue(Sheet.DESCRIPTION) ?: 5
private val Map<String, Int>?.history: Int get() = this?.getValue(Sheet.HISTORY) ?: 6
class ServiceNotReady : Exception()
class IncompatibleSheetStructure(message: String?) : Exception(message)
companion object {
const val TAG = "LexiconRepository"
}
private object Sheet {
const val ID = "1oL9Nu5y37BPEbKxHre4TN9o8nrgy2JQoON4RRkdAHMs"
const val LEXIQUE = "Lexique"
const val KNOWN_COLUMN = 7
const val NAME = "Nom"
const val DIMINUTIVE = "Diminutif"
const val GENDER = "Sexe"
const val RACE = "Race"
const val PORTRAIT = "Portrait"
const val DESCRIPTION = "Description"
const val HISTORY = "Histoire"
}
val columns =
listOf("Nom", "Diminutif", "Sexe", "Race", "Portrait", "Description", "Histoire")
private object Gender {
const val MALE = "Male"
const val FEMALE = "Femelle"
}
private object Race {
const val ELF = "Elfe"
const val HALFLING = "Halfelin"
const val HUMAN = "Humain"
const val DWARF = "Nain"
const val HALF_ELF = "Demi-Elfe"
const val HALF_ORC = "Demi-Orc"
const val DRAGONBORN = "Drakéide"
const val GNOME = "Gnome"
const val TIEFLING = "Tieffelin"
const val AARAKOCRA = "Aarakocra"
const val GENASI = "Génasi"
const val DEEP_GNOME = "Gnome des Profondeurs"
const val GOLIATH = "Goliath"
}
}

View file

@ -2,11 +2,13 @@ package com.pixelized.rplexicon.ui.screens.authentication
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.LocalTextStyle
@ -22,6 +24,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
@ -81,23 +84,28 @@ private fun AuthenticationScreenContent(
version: VersionViewModel.Version,
onGoogleSignIn: () -> Unit,
) {
val typography = MaterialTheme.typography
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.Bottom),
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Bottom),
horizontalAlignment = Alignment.End,
) {
Button(
modifier = Modifier
.padding(all = 16.dp)
.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(),
.fillMaxWidth()
.border(
width = 2.dp,
color = MaterialTheme.colorScheme.primary,
shape = CircleShape,
),
colors = ButtonDefaults.outlinedButtonColors(),
onClick = onGoogleSignIn,
) {
Text(text = rememeberGoogleStringResource())
}
Text(
style = MaterialTheme.typography.labelSmall,
style = remember { typography.labelSmall.copy(fontStyle = FontStyle.Italic) },
text = version.toText(),
)
}

View file

@ -3,6 +3,7 @@ package com.pixelized.rplexicon.ui.screens.detail
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.net.Uri
import androidx.annotation.StringRes
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@ -48,6 +49,7 @@ 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.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
@ -63,8 +65,8 @@ import com.skydoves.landscapist.glide.GlideImage
data class CharacterDetailUio(
val name: String?,
val diminutive: String?,
val gender: String?,
val race: String?,
@StringRes val gender: Int,
@StringRes val race: Int,
val portrait: List<Uri>,
val description: String?,
val history: String?,
@ -111,7 +113,9 @@ private fun CharacterDetailScreenContent(
)
}
},
title = { },
title = {
Text(text = stringResource(id = R.string.detail_title))
},
)
},
) { paddingValues ->
@ -175,24 +179,20 @@ private fun CharacterDetailScreenContent(
modifier = Modifier.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
item.value.gender?.let {
Text(
style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) },
text = it,
)
}
item.value.race?.let {
Text(
style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) },
text = it,
)
}
Text(
style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) },
text = stringResource(id = item.value.gender),
)
Text(
style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) },
text = stringResource(id = item.value.race),
)
}
item.value.description?.let {
Text(
modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp),
style = typography.titleMedium,
text = "Description",
text = stringResource(id = R.string.detail_description),
)
Text(
modifier = Modifier.padding(horizontal = 16.dp),
@ -211,7 +211,7 @@ private fun CharacterDetailScreenContent(
Text(
modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp),
style = typography.titleMedium,
text = "Histoire",
text = stringResource(id = R.string.detail_history),
)
Text(
modifier = Modifier.padding(horizontal = 16.dp),
@ -223,7 +223,7 @@ private fun CharacterDetailScreenContent(
Text(
modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp),
style = typography.titleMedium,
text = "Portrait",
text = stringResource(id = R.string.detail_portrait),
)
LazyRow(
contentPadding = PaddingValues(horizontal = 16.dp),
@ -281,8 +281,8 @@ private fun CharacterDetailScreenContentPreview() {
CharacterDetailUio(
name = "Brulkhai",
diminutive = "./ Bru",
gender = "female",
race = "Demi-Orc",
gender = R.string.gender_female,
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"),

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.model.Lexicon
import com.pixelized.rplexicon.repository.LexiconRepository
import com.pixelized.rplexicon.ui.navigation.screens.characterDetailArgument
@ -23,11 +24,26 @@ class CharacterDetailViewModel @Inject constructor(
name = source.name,
diminutive = source.diminutive?.let { "./ $it" },
gender = when (source.gender) {
Lexicon.Gender.MALE -> "Male"
Lexicon.Gender.FEMALE -> "Femelle"
Lexicon.Gender.UNDETERMINED -> "Inconnu"
Lexicon.Gender.MALE -> R.string.gender_male
Lexicon.Gender.FEMALE -> R.string.gender_female
Lexicon.Gender.UNDETERMINED -> R.string.gender_undetermined
},
race = when (source.race) {
Lexicon.Race.ELF -> R.string.race_elf
Lexicon.Race.HALFLING -> R.string.race_halfling
Lexicon.Race.HUMAN -> R.string.race_human
Lexicon.Race.DWARF -> R.string.race_dwarf
Lexicon.Race.HALF_ELF -> R.string.race_half_elf
Lexicon.Race.HALF_ORC -> R.string.race_half_orc
Lexicon.Race.DRAGONBORN -> R.string.race_dragonborn
Lexicon.Race.GNOME -> R.string.race_gnome
Lexicon.Race.TIEFLING -> R.string.race_tiefling
Lexicon.Race.AARAKOCRA -> R.string.race_aarakocra
Lexicon.Race.GENASI -> R.string.race_genasi
Lexicon.Race.DEEP_GNOME -> R.string.race_deep_gnome
Lexicon.Race.GOLIATH -> R.string.race_goliath
Lexicon.Race.UNDETERMINED -> R.string.race_undetermined
},
race = source.race,
portrait = source.portrait,
description = source.description,
history = source.history,

View file

@ -2,10 +2,14 @@ package com.pixelized.rplexicon.ui.screens.lexicon
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@ -13,21 +17,37 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
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.rplexicon.R
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.placeholder
@Stable
data class LexiconItemUio(
val id: Int,
val name: String,
val diminutive: String?,
val gender: String?,
val race: String?,
)
@StringRes val gender: Int,
@StringRes val race: Int,
val placeholder: Boolean = false,
) {
companion object {
val Brulkhai = LexiconItemUio(
id = 0,
name = "Brulkhai",
diminutive = "Bru",
gender = R.string.gender_female_short,
race = R.string.race_half_orc,
placeholder = true,
)
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun LexiconItem(
modifier: Modifier = Modifier,
@ -39,17 +59,23 @@ fun LexiconItem(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Row(
FlowRow(
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
modifier = Modifier.alignByBaseline(),
modifier = Modifier
.alignByBaseline()
.placeholder { item.placeholder },
style = remember { typography.bodyLarge.copy(fontWeight = FontWeight.Bold) },
maxLines = 1,
text = item.name,
)
Text(
modifier = Modifier.alignByBaseline(),
modifier = Modifier
.alignByBaseline()
.placeholder { item.placeholder },
style = typography.labelMedium,
maxLines = 1,
text = item.diminutive ?: ""
)
}
@ -57,12 +83,20 @@ fun LexiconItem(
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
modifier = Modifier
.alignByBaseline()
.placeholder { item.placeholder },
style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) },
text = item.gender ?: ""
maxLines = 1,
text = stringResource(id = item.gender)
)
Text(
modifier = Modifier
.alignByBaseline()
.placeholder { item.placeholder },
style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) },
text = item.race ?: ""
maxLines = 1,
text = stringResource(id = item.race)
)
}
}
@ -76,13 +110,15 @@ private fun LexiconItemContentPreview() {
LexiconTheme {
Surface {
LexiconItem(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 16.dp),
item = LexiconItemUio(
id = 0,
name = "Brulkhai",
diminutive = "Bru",
gender = "f.",
race = "Demi-Orc",
gender = R.string.gender_female_short,
race = R.string.race_half_orc,
)
)
}

View file

@ -6,6 +6,8 @@ import android.content.res.Configuration.UI_MODE_NIGHT_NO
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.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
@ -16,14 +18,14 @@ 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.foundation.shape.CircleShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.pullrefresh.PullRefreshIndicator
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.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@ -38,20 +40,20 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
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.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
import com.pixelized.rplexicon.ui.screens.lexicon.LexiconErrorUio.Default
import com.pixelized.rplexicon.ui.screens.lexicon.LexiconErrorUio.Permission
import com.pixelized.rplexicon.ui.screens.lexicon.LexiconErrorUio.Structure
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
@ -61,6 +63,9 @@ sealed class LexiconErrorUio {
@Stable
data class Permission(val intent: Intent) : LexiconErrorUio()
@Stable
object Structure : LexiconErrorUio()
@Stable
object Default : LexiconErrorUio()
}
@ -75,7 +80,7 @@ fun LexiconScreen(
val screen = LocalScreenNavHost.current
val refresh = rememberPullRefreshState(
refreshing = viewModel.isLoading.value,
refreshing = false,
onRefresh = {
scope.launch {
viewModel.fetchLexicon()
@ -113,7 +118,9 @@ fun LexiconScreen(
}
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class,
ExperimentalFoundationApi::class
)
@Composable
private fun LexiconScreenContent(
modifier: Modifier = Modifier,
@ -156,9 +163,9 @@ private fun LexiconScreenContent(
// },
// )
}
) {
) { padding ->
Box(
modifier = Modifier.padding(paddingValues = it),
modifier = Modifier.padding(paddingValues = padding),
contentAlignment = Alignment.TopCenter,
) {
LazyColumn(
@ -171,30 +178,72 @@ private fun LexiconScreenContent(
bottom = 8.dp + 16.dp // + 56.dp + 16.dp,
),
) {
items(items = items.value) { item ->
LexiconItem(
modifier = Modifier
.clickable { onItem(item) }
.fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 16.dp),
item = item,
)
if (items.value.isEmpty()) {
items(
count = 6,
key = { it },
contentType = { "Lexicon" },
) {
LexiconItem(
modifier = Modifier
.animateItemPlacement()
.fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 16.dp),
item = LexiconItemUio.Brulkhai,
)
}
} else {
items(
items = items.value,
key = { it.id },
contentType = { "Lexicon" },
) {
LexiconItem(
modifier = Modifier
.animateItemPlacement()
.clickable { onItem(it) }
.fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 16.dp),
item = it,
)
}
}
}
PullRefreshIndicator(
refreshing = refreshing.value,
state = refreshState,
Loader(
refreshState = refreshState,
refreshing = refreshing,
)
}
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun Loader(
refreshState: PullRefreshState,
refreshing: State<Boolean>,
) {
if (refreshing.value) {
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.clip(shape = CircleShape)
)
}
PullRefreshIndicator(
refreshing = false,
state = refreshState,
)
}
@Composable
fun HandleError(
errors: SharedFlow<LexiconErrorUio>,
onLexiconPermissionGranted: suspend () -> Unit,
) {
val context = LocalContext.current
val snack = LocalSnack.current
val scope = rememberCoroutineScope()
@ -212,7 +261,8 @@ fun HandleError(
errors.collect { error ->
when (error) {
is Permission -> launcher.launch(error.intent)
is Default -> snack.showSnackbar(message = "Oops")
is Structure -> snack.showSnackbar(message = context.getString(R.string.error_structure))
is Default -> snack.showSnackbar(message = context.getString(R.string.error_generic))
}
}
}
@ -238,11 +288,11 @@ private fun LexiconScreenContentPreview() {
mutableStateOf(
listOf(
LexiconItemUio(
id = 2,
id = 0,
name = "Brulkhai",
diminutive = "Bru",
gender = "f.",
race = "Demi-Orc",
gender = R.string.gender_female_short,
race = R.string.race_half_orc,
)
)
)

View file

@ -6,8 +6,10 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.model.Lexicon
import com.pixelized.rplexicon.repository.LexiconRepository
import com.pixelized.rplexicon.repository.LexiconRepository.IncompatibleSheetStructure
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
@ -37,11 +39,26 @@ class LexiconViewModel @Inject constructor(
name = item.name,
diminutive = item.diminutive?.takeIf { it.isNotBlank() }?.let { "./ $it" },
gender = when (item.gender) {
Lexicon.Gender.MALE -> "m."
Lexicon.Gender.FEMALE -> "f."
Lexicon.Gender.UNDETERMINED -> "u."
Lexicon.Gender.MALE -> R.string.gender_male_short
Lexicon.Gender.FEMALE -> R.string.gender_female_short
Lexicon.Gender.UNDETERMINED -> R.string.gender_undetermined_short
},
race = when (item.race) {
Lexicon.Race.ELF -> R.string.race_elf
Lexicon.Race.HALFLING -> R.string.race_halfling
Lexicon.Race.HUMAN -> R.string.race_human
Lexicon.Race.DWARF -> R.string.race_dwarf
Lexicon.Race.HALF_ELF -> R.string.race_half_elf
Lexicon.Race.HALF_ORC -> R.string.race_half_orc
Lexicon.Race.DRAGONBORN -> R.string.race_dragonborn
Lexicon.Race.GNOME -> R.string.race_gnome
Lexicon.Race.TIEFLING -> R.string.race_tiefling
Lexicon.Race.AARAKOCRA -> R.string.race_aarakocra
Lexicon.Race.GENASI -> R.string.race_genasi
Lexicon.Race.DEEP_GNOME -> R.string.race_deep_gnome
Lexicon.Race.GOLIATH -> R.string.race_goliath
Lexicon.Race.UNDETERMINED -> R.string.race_undetermined
},
race = item.race,
)
}.sortedBy { it.name }
}
@ -61,6 +78,9 @@ class LexiconViewModel @Inject constructor(
catch (exception: UserRecoverableAuthIOException) {
Log.e(TAG, exception.message, exception)
_error.emit(LexiconErrorUio.Permission(intent = exception.intent))
} catch (exception: IncompatibleSheetStructure) {
Log.e(TAG, exception.message, exception)
_error.emit(LexiconErrorUio.Structure)
}
// default exception
catch (exception: Exception) {

View file

@ -1,11 +1,45 @@
package com.pixelized.rplexicon.ui.theme
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.darkColorScheme
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
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
@Stable
@Immutable
class LexiconColors(
val base: ColorScheme,
val placeholder: Color,
)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
@Stable
fun darkColorScheme(
base: ColorScheme = darkColorScheme(
primary = BaseDark.Purple80,
secondary = BaseDark.PurpleGrey80,
tertiary = BaseDark.Pink80,
onPrimary = Color.White,
),
placeholder: Color = Color(red = 49, green = 48, blue = 51),
) = LexiconColors(
base = base,
placeholder = placeholder,
)
@Stable
fun lightColorScheme(
base: ColorScheme = lightColorScheme(
primary = BaseLight.Purple40,
secondary = BaseLight.PurpleGrey40,
tertiary = BaseLight.Pink40,
onPrimary = Color.White,
),
placeholder: Color = Color(red = 230, green = 225, blue = 229),
) = LexiconColors(
base = base,
placeholder = placeholder,
)

View file

@ -3,27 +3,23 @@ package com.pixelized.rplexicon.ui.theme
import android.app.Activity
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.runtime.Stable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80,
onPrimary = Color.White,
)
val LocalLexiconTheme = compositionLocalOf<LexiconTheme> {
error("LocalLexiconTheme not ready yet.")
}
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40,
onPrimary = Color.White,
@Stable
data class LexiconTheme(
val colorScheme: LexiconColors,
)
@Composable
@ -31,16 +27,20 @@ fun LexiconTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = when {
darkTheme -> DarkColorScheme
else -> LightColorScheme
val lexiconTheme = remember {
LexiconTheme(
colorScheme = when (darkTheme) {
true -> darkColorScheme()
else -> lightColorScheme()
}
)
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
colorScheme.background.toArgb().let {
lexiconTheme.colorScheme.base.background.toArgb().let {
window.statusBarColor = it
window.navigationBarColor = it
}
@ -51,9 +51,13 @@ fun LexiconTheme(
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
CompositionLocalProvider(
LocalLexiconTheme provides lexiconTheme,
) {
MaterialTheme(
colorScheme = lexiconTheme.colorScheme.base,
typography = Typography,
content = content
)
}
}

View file

@ -0,0 +1,57 @@
package com.pixelized.rplexicon.ui.theme.colors
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
@Immutable
object BaseDark {
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
}
@Immutable
object BaseLight {
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
}
@Immutable
object BaseColorPalette {
val VeryDarkBlue: Color = Color(0xFF09179D)
val DarkBlue: Color = Color(0xFF1A2BDB)
val Blue: Color = Color(0xFF2970F2)
val LightBlue: Color = Color(0xFF1A91DB)
val VeryLightBlue: Color = Color(0xFF1EDDEF)
val VeryDarkPurple: Color = Color(0xFF5F0E9E)
val DarkPurple: Color = Color(0xFF8330DB)
val Purple: Color = Color(0xFF9B54C3)
val LightPurple: Color = Color(0xFFBC52D9)
val VeryLightPurple: Color = Color(0xFFC856D1)
val VeryDarkGreen: Color = Color(0xFF16544A)
val DarkGreen: Color = Color(0xFF207A6B)
val Green: Color = Color(0xFF269482)
val LightGreen: Color = Color(0xFF2AA18D)
val VeryLightGreen: Color = Color(0xFF3AE0C5)
val VeryDarkRed: Color = Color(0xFF631221)
val DarkRed: Color = Color(0xFFA21D36)
val Red: Color = Color(0xFFC92443)
val LightRed: Color = Color(0xFFE32849)
val VeryLightRed: Color = Color(0xFFF02B4F)
val VeryDarkPink: Color = Color(0xFF960064)
val DarkPink: Color = Color(0xFFBD007E)
val Pink: Color = Color(0xFFD6008F)
val LightPink: Color = Color(0xFFE35BB5)
val VeryLightPink: Color = Color(0xFFFF66CC)
val VeryDarkYellow: Color = Color(0xFFB76036)
val DarkYellow: Color = Color(0xFFD48341)
val Yellow: Color = Color(0xFFF3A850)
val LightYellow: Color = Color(0xFFF5BF63)
val VeryLightYellow: Color = Color(0xFFF9D679)
val VeryDarkGrey: Color = Color(0xFF1D1D1D)
val DarkGrey: Color = Color(0xFF424242)
val Grey: Color = Color(0xFF919195)
val LightGrey: Color = Color(0xFFDFDFDF)
val VeryLightGrey: Color = Color(0xFFF9F9F9)
}

View file

@ -0,0 +1,10 @@
package com.pixelized.rplexicon.ui.theme.colors
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
@Immutable
object ShadowPalette {
val system: Color = Color.Black.copy(alpha = 0.37f)
val scrim: Color = Color.Black.copy(alpha = 0.37f)
}

View file

@ -0,0 +1,12 @@
package com.pixelized.rplexicon.utilitary.extentions
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.ui.theme.LocalLexiconTheme
val MaterialTheme.lexicon: LexiconTheme
@Composable
@Stable
get() = LocalLexiconTheme.current

View file

@ -0,0 +1,30 @@
package com.pixelized.rplexicon.utilitary.extentions
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.spring
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import com.google.accompanist.placeholder.PlaceholderHighlight
import com.google.accompanist.placeholder.placeholder
@Composable
fun Modifier.placeholder(
color: Color = MaterialTheme.lexicon.colorScheme.placeholder,
shape: Shape = CircleShape,
highlight: PlaceholderHighlight? = null,
placeholderFadeTransitionSpec: @Composable Transition.Segment<Boolean>.() -> FiniteAnimationSpec<Float> = { spring() },
contentFadeTransitionSpec: @Composable Transition.Segment<Boolean>.() -> FiniteAnimationSpec<Float> = { spring() },
visible: () -> Boolean,
): Modifier = placeholder(
visible = visible(),
color = color,
shape = shape,
highlight = highlight,
placeholderFadeTransitionSpec = placeholderFadeTransitionSpec,
contentFadeTransitionSpec = contentFadeTransitionSpec,
)

View file

@ -1,5 +1,35 @@
<resources>
<string name="app_name">Lexique</string>
<string name="app_name">Rp-Lexique</string>
<string name="error_generic">Ah !? y\'a un truc qui foire quelque part.</string>
<string name="error_structure">La structure du fichier semble avoir changé et n\'est plus compatible avec cette application.</string>
<string name="gender_male">Mâle</string>
<string name="gender_female">Femelle</string>
<string name="gender_undetermined">Indéterminé</string>
<string name="gender_male_short">m.</string>
<string name="gender_female_short">f.</string>
<string name="gender_undetermined_short">i.</string>
<string name="race_elf">Elfe</string>
<string name="race_halfling">Halfelin</string>
<string name="race_human">Humain</string>
<string name="race_dwarf">Nain</string>
<string name="race_half_elf">Demi-Elfe</string>
<string name="race_half_orc">Demi-Orc</string>
<string name="race_dragonborn">Drakéide</string>
<string name="race_gnome">Gnome</string>
<string name="race_tiefling">Tieffelin</string>
<string name="race_aarakocra">Aarakocra</string>
<string name="race_genasi">Génasi</string>
<string name="race_deep_gnome">Gnome des Profondeurs</string>
<string name="race_goliath">Goliath</string>
<string name="race_undetermined">Indéterminé</string>
<string name="action_google_sign_in">Se connecter avec</string>
<string name="detail_title">Détails du personnage</string>
<string name="detail_description">Description</string>
<string name="detail_history">History</string>
<string name="detail_portrait">Portrait</string>
</resources>

View file

@ -1,5 +1,35 @@
<resources>
<string name="app_name">Lexique</string>
<string name="app_name">Rp-Lexicon</string>
<string name="error_generic">Oups, it should not be rocket science.</string>
<string name="error_structure">The file structure appears to have changed and is no longer compatible with this application</string>
<string name="gender_male">Male</string>
<string name="gender_female">Female</string>
<string name="gender_undetermined">Undetermined</string>
<string name="gender_male_short">m.</string>
<string name="gender_female_short">f.</string>
<string name="gender_undetermined_short">u.</string>
<string name="race_elf">Elf</string>
<string name="race_halfling">Halfling</string>
<string name="race_human">Human</string>
<string name="race_dwarf">Dwarf</string>
<string name="race_half_elf">Half-elf</string>
<string name="race_half_orc">Half-Orc</string>
<string name="race_dragonborn">Dragonborn</string>
<string name="race_gnome">Gnome</string>
<string name="race_tiefling">Tiefling</string>
<string name="race_aarakocra">Aarakocra</string>
<string name="race_genasi">Genasi</string>
<string name="race_deep_gnome">Deep Gnome</string>
<string name="race_goliath">Goliath</string>
<string name="race_undetermined">Undetermined</string>
<string name="action_google_sign_in">Sign in with</string>
<string name="detail_title">Character\'s details</string>
<string name="detail_description">Description</string>
<string name="detail_history">Histoire</string>
<string name="detail_portrait">Portrait</string>
</resources>