From 6cfd673335ef1dbb5e5726008636ef8c0920b25c Mon Sep 17 00:00:00 2001 From: "Andres Gomez, Thomas (ITDV CC) - AF (ext)" Date: Sun, 16 Jul 2023 09:40:46 +0200 Subject: [PATCH] Add Spreadsheet API to the project. --- app/build.gradle.kts | 11 +- .../com/pixelized/rplexicon/MainActivity.kt | 11 +- .../rplexicon/module/NetworkModule.kt | 24 --- .../rplexicon/network/IGoogleSpreadSheet.kt | 19 --- .../repository/AuthenticationRepository.kt | 44 +++++ .../rplexicon/repository/LexiconRepository.kt | 151 +++++++++++------- .../authentication/AuthenticationScreen.kt | 2 +- .../authentication/AuthenticationViewModel.kt | 28 ++-- .../screens/detail/CharacterDetailScreen.kt | 12 +- .../ui/screens/lexicon/LexiconScreen.kt | 56 +++++++ .../ui/screens/lexicon/LexiconViewModel.kt | 59 +++++-- 11 files changed, 274 insertions(+), 143 deletions(-) delete mode 100644 app/src/main/java/com/pixelized/rplexicon/module/NetworkModule.kt delete mode 100644 app/src/main/java/com/pixelized/rplexicon/network/IGoogleSpreadSheet.kt create mode 100644 app/src/main/java/com/pixelized/rplexicon/repository/AuthenticationRepository.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 26e777c..9a0977f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -99,13 +99,18 @@ dependencies { // Google service implementation("com.google.android.gms:play-services-auth:20.6.0") + implementation( + dependencyNotation = "com.google.api-client:google-api-client-android:1.23.0", + dependencyConfiguration = { exclude("org.apache.httpcomponents") }, + ) + implementation( + dependencyNotation = "com.google.apis:google-api-services-sheets:v4-rev20220927-2.0.0", + dependencyConfiguration = { exclude("org.apache.httpcomponents") }, + ) // Image implementation("com.github.skydoves:landscapist-glide:2.1.11") kapt("com.github.bumptech.glide:compiler:4.14.2") // this have to be align with landscapist-glide - - // Retrofit : Network - implementation("com.squareup.retrofit2:retrofit:2.9.0") } private val NamedDomainObjectContainer.pixelized get() = this.getByName("pixelized") \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/MainActivity.kt b/app/src/main/java/com/pixelized/rplexicon/MainActivity.kt index 17d887d..8743622 100644 --- a/app/src/main/java/com/pixelized/rplexicon/MainActivity.kt +++ b/app/src/main/java/com/pixelized/rplexicon/MainActivity.kt @@ -8,8 +8,11 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier import com.pixelized.rplexicon.ui.navigation.ScreenNavHost @@ -17,6 +20,8 @@ import com.pixelized.rplexicon.ui.theme.LexiconTheme import dagger.hilt.android.AndroidEntryPoint val LocalActivity = staticCompositionLocalOf { error("Activity not available") } +val LocalSnack = + staticCompositionLocalOf { error("SnackbarHostState not available") } @AndroidEntryPoint class MainActivity : ComponentActivity() { @@ -27,7 +32,8 @@ class MainActivity : ComponentActivity() { setContent { LexiconTheme { CompositionLocalProvider( - LocalActivity provides this + LocalActivity provides this, + LocalSnack provides remember { SnackbarHostState() } ) { Scaffold( content = { padding -> @@ -39,6 +45,9 @@ class MainActivity : ComponentActivity() { ) { ScreenNavHost() } + }, + snackbarHost = { + SnackbarHost(hostState = LocalSnack.current) } ) } diff --git a/app/src/main/java/com/pixelized/rplexicon/module/NetworkModule.kt b/app/src/main/java/com/pixelized/rplexicon/module/NetworkModule.kt deleted file mode 100644 index 843aceb..0000000 --- a/app/src/main/java/com/pixelized/rplexicon/module/NetworkModule.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.pixelized.rplexicon.module - -import com.pixelized.rplexicon.network.IGoogleSpreadSheet -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import retrofit2.Retrofit -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -class NetworkModule { - - @Provides - @Singleton - fun provideGoogleSpreadSheet(): IGoogleSpreadSheet { - val retrofit: Retrofit = Retrofit.Builder() - .baseUrl(IGoogleSpreadSheet.HOST) - .build() - - return retrofit.create(IGoogleSpreadSheet::class.java) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/network/IGoogleSpreadSheet.kt b/app/src/main/java/com/pixelized/rplexicon/network/IGoogleSpreadSheet.kt deleted file mode 100644 index 2e2c6c7..0000000 --- a/app/src/main/java/com/pixelized/rplexicon/network/IGoogleSpreadSheet.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.pixelized.rplexicon.network - -import retrofit2.http.GET - -interface IGoogleSpreadSheet { - - @GET("spreadsheets/d/$ID/edit#gid=$LEXICON_GID") - fun getLexicon() - - @GET("spreadsheets/d/$ID/edit#gid=$META_DATA_GID") - fun getMetaData() - - companion object { - const val HOST = "https://docs.google.com/" - const val ID = "1oL9Nu5y37BPEbKxHre4TN9o8nrgy2JQoON4RRkdAHMs" - const val LEXICON_GID = 0 - const val META_DATA_GID = "957635233" - } -} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/repository/AuthenticationRepository.kt b/app/src/main/java/com/pixelized/rplexicon/repository/AuthenticationRepository.kt new file mode 100644 index 0000000..d59c8e7 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/repository/AuthenticationRepository.kt @@ -0,0 +1,44 @@ +package com.pixelized.rplexicon.repository + +import android.content.Context +import androidx.compose.runtime.State +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.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential +import com.google.api.client.util.ExponentialBackOff +import com.google.api.services.sheets.v4.SheetsScopes +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import javax.inject.Singleton + +@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) + + val credential: GoogleAccountCredential + get() { + val credential = GoogleAccountCredential + .usingOAuth2( + context, listOf( + SheetsScopes.SPREADSHEETS, + SheetsScopes.SPREADSHEETS_READONLY, + ) + ) + .setBackOff(ExponentialBackOff()) + + credential.selectedAccount = account?.account + + return credential + } + + fun updateAuthenticationState() { + _isAuthenticated.value = account != null + } +} \ 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 fd18f96..28fa9a6 100644 --- a/app/src/main/java/com/pixelized/rplexicon/repository/LexiconRepository.kt +++ b/app/src/main/java/com/pixelized/rplexicon/repository/LexiconRepository.kt @@ -1,80 +1,115 @@ package com.pixelized.rplexicon.repository import android.net.Uri +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +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 +import com.google.api.services.sheets.v4.model.ValueRange import com.pixelized.rplexicon.model.Lexicon -import com.pixelized.rplexicon.network.IGoogleSpreadSheet -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Singleton @Singleton class LexiconRepository @Inject constructor( - private val spreadSheet: IGoogleSpreadSheet, + private val authenticationRepository: AuthenticationRepository ) { - private val scope = CoroutineScope(Dispatchers.IO) + private val sheetService: Sheets? by derivedStateOf { + when (authenticationRepository.isAuthenticated.value) { + true -> Sheets + .Builder( + AndroidHttp.newCompatibleTransport(), + GsonFactory(), + authenticationRepository.credential, + ) + .setApplicationName("RP-Lexique") + .build() + + else -> null + } + } private val _data = MutableStateFlow>(emptyList()) val data: StateFlow> get() = _data - init { - scope.launch { - _data.emit(sample()) + @Throws(ServiceNotReady::class, Exception::class) + suspend fun fetchLexicon(): ValueRange? { + val service = sheetService + return if (service == null) { + throw ServiceNotReady() + } else { + withContext(Dispatchers.IO) { + val request = service.spreadsheets().values().get(ID, LEXIQUE) + request.execute() + } } } - 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 HOST = "https://docs.google.com/" + const val ID = "1oL9Nu5y37BPEbKxHre4TN9o8nrgy2JQoON4RRkdAHMs" + const val LEXICON_GID = 0 + const val META_DATA_GID = "957635233" + const val LEXIQUE = "Lexique" } + + class ServiceNotReady : Exception() +} + + +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, + ), + ) } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/authentication/AuthenticationScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/authentication/AuthenticationScreen.kt index 5ace864..031f85e 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/authentication/AuthenticationScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/authentication/AuthenticationScreen.kt @@ -42,7 +42,7 @@ fun AuthenticationScreen( ) { val screen = LocalScreenNavHost.current val activity = LocalActivity.current - val state = viewModel.rememberAuthenticationState(activity = activity) + val state = viewModel.rememberAuthenticationState() Surface { AuthenticationScreenContent( 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 d198388..28996d8 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,51 +16,41 @@ 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.signin.GoogleSignIn -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 import javax.inject.Inject @HiltViewModel class AuthenticationViewModel @Inject constructor( application: Application, + private val repository: AuthenticationRepository, ) : AndroidViewModel(application) { private val context: Context get() = getApplication() private var launcher: ManagedActivityResultLauncher? = null - private val account: GoogleSignInAccount? by lazy { - GoogleSignIn.getLastSignedInAccount(application) - } - - private val lastGoogleToken: String? by lazy { - GoogleSignIn.getLastSignedInAccount(application)?.idToken - } - private val state = mutableStateOf( - when (account) { - null -> Authentication.Initial - else -> Authentication.Success + when (repository.isAuthenticated.value) { + true -> Authentication.Success + else -> Authentication.Initial } ) @Composable - fun rememberAuthenticationState(activity: Activity): State { + fun rememberAuthenticationState(): State { launcher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartIntentSenderForResult(), onResult = { if (it.resultCode == Activity.RESULT_OK) { - val credential = Identity - .getSignInClient(activity) - .getSignInCredentialFromIntent(it.data) state.value = Authentication.Success + repository.updateAuthenticationState() } else { state.value = Authentication.Failure + repository.updateAuthenticationState() } }, ) - return state } @@ -79,10 +69,12 @@ class AuthenticationViewModel @Inject constructor( ) } catch (e: SendIntentException) { state.value = Authentication.Failure + repository.updateAuthenticationState() } } .addOnFailureListener { e -> 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 ccfd565..dbc5bdf 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 @@ -5,6 +5,7 @@ import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.net.Uri import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -77,7 +78,7 @@ fun CharacterDetailScreen( modifier = Modifier.fillMaxSize(), item = viewModel.character, onBack = { }, - onItem = { }, + onImage = { }, ) } } @@ -89,7 +90,7 @@ private fun CharacterDetailScreenContent( state: ScrollState = rememberScrollState(), item: State, onBack: () -> Unit, - onItem: () -> Unit, + onImage: (Uri) -> Unit, ) { val colorScheme = MaterialTheme.colorScheme val typography = MaterialTheme.typography @@ -227,7 +228,9 @@ private fun CharacterDetailScreenContent( ) { items(items = item.value.portrait) { GlideImage( - modifier = Modifier.height(320.dp), + modifier = Modifier + .clickable { onImage(it) } + .height(320.dp), imageModel = { it }, imageOptions = ImageOptions( contentScale = ContentScale.FillHeight @@ -239,7 +242,6 @@ private fun CharacterDetailScreenContent( } } } - } } @@ -299,7 +301,7 @@ private fun CharacterDetailScreenContentPreview() { modifier = Modifier.fillMaxSize(), item = character, onBack = { }, - onItem = { }, + onImage = { }, ) } } 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 2726989..a9c9885 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 @@ -1,7 +1,11 @@ package com.pixelized.rplexicon.ui.screens.lexicon +import android.app.Activity +import android.content.Intent 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.foundation.clickable import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -11,16 +15,33 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.Surface import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import com.pixelized.rplexicon.LocalSnack import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost import com.pixelized.rplexicon.ui.navigation.navigateToCharacterDetail +import com.pixelized.rplexicon.ui.screens.lexicon.LexiconErrorUio.Default +import com.pixelized.rplexicon.ui.screens.lexicon.LexiconErrorUio.Permission import com.pixelized.rplexicon.ui.theme.LexiconTheme +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.launch + +@Stable +sealed class LexiconErrorUio { + @Stable + data class Permission(val intent: Intent) : LexiconErrorUio() + + @Stable + object Default : LexiconErrorUio() +} @Composable fun LexiconScreen( @@ -35,6 +56,13 @@ fun LexiconScreen( screen.navigateToCharacterDetail(id = "") }, ) + + HandleError( + errors = viewModel.error, + onLexiconPermissionGranted = { + viewModel.fetchLexicon() + } + ) } } @@ -60,6 +88,34 @@ private fun LexiconScreenContent( } } +@Composable +fun HandleError( + errors: SharedFlow, + onLexiconPermissionGranted: suspend () -> Unit, +) { + val snack = LocalSnack.current + val scope = rememberCoroutineScope() + + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult(), + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + scope.launch { + onLexiconPermissionGranted() + } + } + } + + LaunchedEffect(key1 = "LexiconErrorManagement") { + errors.collect { error -> + when (error) { + is Permission -> launcher.launch(error.intent) + is Default -> snack.showSnackbar(message = "Oops") + } + } + } +} + @Composable @Preview(uiMode = UI_MODE_NIGHT_NO) @Preview(uiMode = UI_MODE_NIGHT_YES) 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 0b30f61..361741a 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 @@ -1,12 +1,17 @@ package com.pixelized.rplexicon.ui.screens.lexicon +import android.util.Log import androidx.compose.runtime.State 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.model.Lexicon import com.pixelized.rplexicon.repository.LexiconRepository import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -15,28 +20,54 @@ class LexiconViewModel @Inject constructor( private val repository: LexiconRepository, ) : ViewModel() { - // TODO : link it to a paginated DataSource private val _items = mutableStateOf>(emptyList()) val items: State> get() = _items + private val _error = MutableSharedFlow() + val error: SharedFlow get() = _error + init { viewModelScope.launch { - repository.data.collect { items -> - _items.value = items.mapNotNull { item -> - item.name?.let { - LexiconItemUio( - name = item.name, - diminutive = item.diminutive?.let { "./ $it" }, - gender = when (item.gender) { - Lexicon.Gender.MALE -> "m." - Lexicon.Gender.FEMALE -> "f." - Lexicon.Gender.UNDETERMINED -> "u." - }, - race = item.race, - ) + 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, + ) + } } } } + + launch { + delay(100) + fetchLexicon() + } } } + + suspend fun fetchLexicon() { + try { + repository.fetchLexicon() + } 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) { + Log.e(TAG, exception.message, exception) + _error.emit(LexiconErrorUio.Default) + } + } + + companion object { + private const val TAG = "LexiconViewModel" + } } \ No newline at end of file