diff --git a/app/src/main/java/com/pixelized/rplexicon/model/Lexicon.kt b/app/src/main/java/com/pixelized/rplexicon/model/Lexicon.kt index dafe0a0..1034591 100644 --- a/app/src/main/java/com/pixelized/rplexicon/model/Lexicon.kt +++ b/app/src/main/java/com/pixelized/rplexicon/model/Lexicon.kt @@ -3,7 +3,8 @@ package com.pixelized.rplexicon.model import android.net.Uri data class Lexicon( - val name: String?, + val id: Int, + val name: String, val diminutive: String?, val gender: Gender = Gender.UNDETERMINED, val race: String?, diff --git a/app/src/main/java/com/pixelized/rplexicon/repository/AuthenticationRepository.kt b/app/src/main/java/com/pixelized/rplexicon/repository/AuthenticationRepository.kt index d59c8e7..55b4481 100644 --- a/app/src/main/java/com/pixelized/rplexicon/repository/AuthenticationRepository.kt +++ b/app/src/main/java/com/pixelized/rplexicon/repository/AuthenticationRepository.kt @@ -1,10 +1,11 @@ package com.pixelized.rplexicon.repository +import android.accounts.Account import android.content.Context import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.mutableStateOf -import com.google.android.gms.auth.api.signin.GoogleSignIn -import com.google.android.gms.auth.api.signin.GoogleSignInAccount +import com.google.android.gms.auth.api.identity.SignInCredential import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential import com.google.api.client.util.ExponentialBackOff import com.google.api.services.sheets.v4.SheetsScopes @@ -16,11 +17,8 @@ import javax.inject.Singleton class AuthenticationRepository @Inject constructor( @ApplicationContext private val context: Context, ) { - private val _isAuthenticated = mutableStateOf(account != null) - val isAuthenticated: State get() = _isAuthenticated - - private val account: GoogleSignInAccount? - get() = GoogleSignIn.getLastSignedInAccount(context) + private val signInCredential = mutableStateOf(null) + val isAuthenticated: State = derivedStateOf { signInCredential.value != null } val credential: GoogleAccountCredential get() { @@ -33,12 +31,16 @@ class AuthenticationRepository @Inject constructor( ) .setBackOff(ExponentialBackOff()) - credential.selectedAccount = account?.account + credential.selectedAccount = signInCredential.value?.let { + Account(it.id, "google") + } return credential } - fun updateAuthenticationState() { - _isAuthenticated.value = account != null + fun updateAuthenticationState( + credential: SignInCredential? = null, + ) { + signInCredential.value = credential } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/repository/LexiconRepository.kt b/app/src/main/java/com/pixelized/rplexicon/repository/LexiconRepository.kt index 28fa9a6..164adc6 100644 --- a/app/src/main/java/com/pixelized/rplexicon/repository/LexiconRepository.kt +++ b/app/src/main/java/com/pixelized/rplexicon/repository/LexiconRepository.kt @@ -3,6 +3,7 @@ package com.pixelized.rplexicon.repository import android.net.Uri import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.core.net.toUri import com.google.api.client.extensions.android.http.AndroidHttp import com.google.api.client.json.gson.GsonFactory import com.google.api.services.sheets.v4.Sheets @@ -37,79 +38,102 @@ class LexiconRepository @Inject constructor( private val _data = MutableStateFlow>(emptyList()) val data: StateFlow> get() = _data - @Throws(ServiceNotReady::class, Exception::class) - suspend fun fetchLexicon(): ValueRange? { + @Throws(ServiceNotReady::class, IncompatibleSheetStructure::class, Exception::class) + suspend fun fetchLexicon() { val service = sheetService - return if (service == null) { + if (service == null) { throw ServiceNotReady() } else { withContext(Dispatchers.IO) { val request = service.spreadsheets().values().get(ID, LEXIQUE) - request.execute() + val data = request.execute() + updateData(data = data) } } } - companion object { - const val HOST = "https://docs.google.com/" - const val ID = "1oL9Nu5y37BPEbKxHre4TN9o8nrgy2JQoON4RRkdAHMs" - const val LEXICON_GID = 0 - const val META_DATA_GID = "957635233" - const val LEXIQUE = "Lexique" + @Throws(IncompatibleSheetStructure::class) + private fun updateData(data: ValueRange?) { + val sheet = data?.values?.sheet() + val lexicon: List = sheet?.mapIndexedNotNull { index, row -> + if (index == 0) { + checkSheetStructure(firstRow = row) + null + } else { + parseCharacterRow(index = index - 1, row = row as? List?) + } + } ?: 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 parseCharacterRow(index: Int, row: List?): 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? + + return if (name != null) { + Lexicon( + id = index, + name = name, + diminutive = diminutive, + gender = when (gender) { + "Male" -> Lexicon.Gender.MALE + "Femelle" -> Lexicon.Gender.FEMALE + else -> Lexicon.Gender.UNDETERMINED + }, + race = race, + portrait = portrait?.split("\n")?.mapNotNull { it.toUriOrNull() } ?: emptyList(), + description = description, + history = history, + ) + } else { + null + } + } + + private fun MutableCollection?.sheet(): List<*>? { + return this?.firstOrNull { + val sheet = it as? ArrayList<*> + sheet != null + } as List<*>? + } + + private fun String?.toUriOrNull(): Uri? = try { + this?.toUri() + } catch (_: Exception) { + null } class ServiceNotReady : Exception() -} + class IncompatibleSheetStructure(message: String?) : Exception(message) -private fun sample(): List { - return listOf( - Lexicon( - name = "Brulkhai", - diminutive = "Bru", - gender = Lexicon.Gender.FEMALE, - race = "Demi-Orc", - portrait = listOf( - Uri.parse("https://drive.google.com/file/d/1a31xJ6DQnzqmGBndG-uo65HNQHPEUJnI/view?usp=sharing"), - ), - description = "Brulkhai, ou plus simplement Bru, est solidement bâti. Elle mesure 192 cm pour 110 kg de muscles lorsqu’elle est en bonne santé. Elle a les cheveux châtains, les yeux noisettes et la peau couleur gris-vert typique de son espèce. D’un tempérament taciturne, elle parle peu et de façon concise. Elle est parfois brutale, aussi bien physiquement que verbalement, Elle ne prend cependant aucun plaisir à malmener ceux qu’elle considère plus faibles qu’elle. D’une nature simple et honnête, elle ne mâche pas ses mots et ne dissimule généralement pas ses pensées. Son intelligence modeste est plus le reflet d’un manque d’éducation et d’une capacité limitée à gérer ses émotions qu’à une débilité congénitale. Elle voue à la force un culte car c’est par son expression qu’elle se sent vraiment vivante et éprouve de grandes difficultés vis à vis de ceux qu’elle nomme foshnu (bébé, chouineur en commun).", - history = null, - ), - Lexicon( - name = "Léandre", - diminutive = null, - gender = Lexicon.Gender.MALE, - race = "Humain", - portrait = emptyList(), - description = null, - history = null, - ), - Lexicon( - name = "Nelia", - diminutive = null, - gender = Lexicon.Gender.FEMALE, - race = "Elfe", - portrait = emptyList(), - description = null, - history = null, - ), - Lexicon( - name = "Tigrane", - diminutive = null, - gender = Lexicon.Gender.MALE, - race = "Tieffelin", - portrait = emptyList(), - description = null, - history = null, - ), - Lexicon( - name = "Unathana", - diminutive = "Una", - gender = Lexicon.Gender.FEMALE, - race = "Demi-Elfe", - portrait = emptyList(), - description = null, - history = null, - ), - ) + companion object { + const val TAG = "LexiconRepository" + const val ID = "1oL9Nu5y37BPEbKxHre4TN9o8nrgy2JQoON4RRkdAHMs" + const val LEXIQUE = "Lexique" + + val columns = + listOf("Nom", "Diminutif", "Sexe", "Race", "Portrait", "Description", "Histoire") + } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/ComposableCharacterDetail.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/ComposableCharacterDetail.kt index 9ddd856..16a9a70 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/ComposableCharacterDetail.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/ComposableCharacterDetail.kt @@ -18,7 +18,7 @@ val CHARACTER_DETAIL_ROUTE = "$ROUTE?${ARG_ID.ARG}" @Stable @Immutable data class CharacterDetailArgument( - val id: String, + val id: Int, ) val SavedStateHandle.characterDetailArgument: CharacterDetailArgument @@ -31,7 +31,7 @@ fun NavGraphBuilder.composableCharacterDetail() { route = CHARACTER_DETAIL_ROUTE, arguments = listOf( navArgument(name = ARG_ID) { - type = NavType.StringType + type = NavType.IntType } ), animation = NavigationAnimation.Push, @@ -41,7 +41,7 @@ fun NavGraphBuilder.composableCharacterDetail() { } fun NavHostController.navigateToCharacterDetail( - id: String, + id: Int, option: NavOptionsBuilder.() -> Unit = {}, ) { val route = "$ROUTE?$ARG_ID=$id" diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/authentication/AuthenticationViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/authentication/AuthenticationViewModel.kt index 28996d8..536df92 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/authentication/AuthenticationViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/authentication/AuthenticationViewModel.kt @@ -16,6 +16,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.AndroidViewModel import com.google.android.gms.auth.api.identity.GetSignInIntentRequest import com.google.android.gms.auth.api.identity.Identity +import com.google.android.gms.auth.api.identity.SignInCredential +import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.pixelized.rplexicon.R import com.pixelized.rplexicon.repository.AuthenticationRepository import dagger.hilt.android.lifecycle.HiltViewModel @@ -43,8 +45,12 @@ class AuthenticationViewModel @Inject constructor( contract = ActivityResultContracts.StartIntentSenderForResult(), onResult = { if (it.resultCode == Activity.RESULT_OK) { + val credential: SignInCredential = Identity + .getSignInClient(context) + .getSignInCredentialFromIntent(it.data) + state.value = Authentication.Success - repository.updateAuthenticationState() + repository.updateAuthenticationState(credential) } else { state.value = Authentication.Failure repository.updateAuthenticationState() diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailScreen.kt index dbc5bdf..bd733e1 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailScreen.kt @@ -54,6 +54,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.pixelized.rplexicon.R +import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.skydoves.landscapist.ImageOptions import com.skydoves.landscapist.glide.GlideImage @@ -73,11 +74,13 @@ data class CharacterDetailUio( fun CharacterDetailScreen( viewModel: CharacterDetailViewModel = hiltViewModel(), ) { + val screen = LocalScreenNavHost.current + Surface { CharacterDetailScreenContent( modifier = Modifier.fillMaxSize(), item = viewModel.character, - onBack = { }, + onBack = { screen.popBackStack() }, onImage = { }, ) } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailViewModel.kt index 1e07059..80f767a 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailViewModel.kt @@ -1,35 +1,36 @@ package com.pixelized.rplexicon.ui.screens.detail -import android.net.Uri import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import com.pixelized.rplexicon.model.Lexicon +import com.pixelized.rplexicon.repository.LexiconRepository +import com.pixelized.rplexicon.ui.navigation.characterDetailArgument import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel -class CharacterDetailViewModel @Inject constructor() : ViewModel() { +class CharacterDetailViewModel @Inject constructor( + private val savedStateHandle: SavedStateHandle, + private val repository: LexiconRepository, +) : ViewModel() { + val character: State = mutableStateOf(init()) - val character: State = mutableStateOf( - CharacterDetailUio( - name = "Brulkhai", - diminutive = "./ Bru", - gender = "female", - race = "Demi-Orc", - portrait = listOf( - Uri.parse("https://cdnb.artstation.com/p/assets/images/images/003/024/889/large/bayard-wu-0716.jpg?1468642855"), - Uri.parse("https://cdnb.artstation.com/p/assets/images/images/003/024/877/large/bayard-wu-0714.jpg?1468642665"), - Uri.parse("https://cdnb.artstation.com/p/assets/images/images/003/024/887/large/bayard-wu-0715.jpg?1468642839"), - Uri.parse("https://cdnb.artstation.com/p/assets/images/images/003/024/891/large/bayard-wu-0623-03.jpg?1468642872"), - Uri.parse("https://cdna.artstation.com/p/assets/images/images/002/869/868/large/bayard-wu-0622-03.jpg?1466664135"), - Uri.parse("https://cdnb.artstation.com/p/assets/images/images/002/869/871/large/bayard-wu-0622-04.jpg?1466664153"), - Uri.parse("https://cdnb.artstation.com/p/assets/images/images/004/347/181/large/bayard-wu-1217.jpg?1482770883"), - Uri.parse("https://cdnb.artstation.com/p/assets/images/images/004/297/635/large/bayard-wu-1215.jpg?1482166826"), - Uri.parse("https://cdnb.artstation.com/p/assets/images/images/004/297/631/large/bayard-wu-1209.jpg?1482166803"), - Uri.parse("https://cdnb.artstation.com/p/assets/images/images/004/297/641/large/bayard-wu-1212.jpg?1482166838"), - ), - description = "Brulkhai, ou plus simplement Bru, est solidement bâti. Elle mesure 192 cm pour 110 kg de muscles lorsqu’elle est en bonne santé. Elle a les cheveux châtains, les yeux noisettes et la peau couleur gris-vert typique de son espèce. D’un tempérament taciturne, elle parle peu et de façon concise. Elle est parfois brutale, aussi bien physiquement que verbalement, Elle ne prend cependant aucun plaisir à malmener ceux qu’elle considère plus faibles qu’elle. D’une nature simple et honnête, elle ne mâche pas ses mots et ne dissimule généralement pas ses pensées. Son intelligence modeste est plus le reflet d’un manque d’éducation et d’une capacité limitée à gérer ses émotions qu’à une débilité congénitale. Elle voue à la force un culte car c’est par son expression qu’elle se sent vraiment vivante et éprouve de grandes difficultés vis à vis de ceux qu’elle nomme foshnu (bébé, chouineur en commun).", - history = null, + private fun init(): CharacterDetailUio { + val source = repository.data.value[savedStateHandle.characterDetailArgument.id] + return CharacterDetailUio( + name = source.name, + diminutive = source.diminutive?.let { "./ $it" }, + gender = when (source.gender) { + Lexicon.Gender.MALE -> "homme" + Lexicon.Gender.FEMALE -> "femme" + Lexicon.Gender.UNDETERMINED -> "inconnu" + }, + race = source.race, + portrait = source.portrait, + description = source.description, + history = source.history, ) - ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconItem.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconItem.kt index d417882..a71a0e2 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconItem.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconItem.kt @@ -21,6 +21,7 @@ import com.pixelized.rplexicon.ui.theme.LexiconTheme @Stable data class LexiconItemUio( + val id: Int, val name: String, val diminutive: String?, val gender: String?, @@ -77,6 +78,7 @@ private fun LexiconItemContentPreview() { LexiconItem( modifier = Modifier.fillMaxWidth(), item = LexiconItemUio( + id = 0, name = "Brulkhai", diminutive = "Bru", gender = "f.", diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconScreen.kt index a9c9885..420fe67 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconScreen.kt @@ -53,7 +53,7 @@ fun LexiconScreen( LexiconScreenContent( items = viewModel.items, onItem = { - screen.navigateToCharacterDetail(id = "") + screen.navigateToCharacterDetail(id = it.id) }, ) @@ -128,6 +128,7 @@ private fun LexiconScreenContentPreview() { mutableStateOf( listOf( LexiconItemUio( + id = 2, name = "Brulkhai", diminutive = "Bru", gender = "f.", diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconViewModel.kt index 361741a..a27328e 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconViewModel.kt @@ -28,40 +28,39 @@ class LexiconViewModel @Inject constructor( init { viewModelScope.launch { - launch { - repository.data.collect { items -> - _items.value = items.mapNotNull { item -> - item.name?.let { - LexiconItemUio( - name = item.name, - diminutive = item.diminutive?.let { "./ $it" }, - gender = when (item.gender) { - Lexicon.Gender.MALE -> "m." - Lexicon.Gender.FEMALE -> "f." - Lexicon.Gender.UNDETERMINED -> "u." - }, - race = item.race, - ) - } - } - } + repository.data.collect { items -> + _items.value = items.map { item -> + LexiconItemUio( + id = item.id, + name = item.name, + diminutive = item.diminutive?.takeIf { it.isNotBlank() }?.let { "./ $it" }, + gender = when (item.gender) { + Lexicon.Gender.MALE -> "h." + Lexicon.Gender.FEMALE -> "f." + Lexicon.Gender.UNDETERMINED -> "u." + }, + race = item.race, + ) + }.sortedBy { it.name } } + } - launch { - delay(100) - fetchLexicon() - } + viewModelScope.launch { + fetchLexicon() } } suspend fun fetchLexicon() { try { repository.fetchLexicon() - } catch (exception: UserRecoverableAuthIOException) { + } + // user need to accept OAuth2 permission. + catch (exception: UserRecoverableAuthIOException) { Log.e(TAG, exception.message, exception) - // user need to accept OAuth2 permission. _error.emit(LexiconErrorUio.Permission(intent = exception.intent)) - } catch (exception: Exception) { + } + // default exception + catch (exception: Exception) { Log.e(TAG, exception.message, exception) _error.emit(LexiconErrorUio.Default) }