Change the parsing mechanist to allow more flexibility.
This commit is contained in:
		
							parent
							
								
									f5c10c5154
								
							
						
					
					
						commit
						fb31de8130
					
				
					 19 changed files with 577 additions and 140 deletions
				
			
		| 
						 | 
				
			
			@ -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
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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(),
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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"),
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
                            )
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
)
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue