Add Spreadsheet API to the project.
This commit is contained in:
parent
f2357c6151
commit
6cfd673335
11 changed files with 274 additions and 143 deletions
|
|
@ -99,13 +99,18 @@ dependencies {
|
||||||
|
|
||||||
// Google service
|
// Google service
|
||||||
implementation("com.google.android.gms:play-services-auth:20.6.0")
|
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
|
// Image
|
||||||
implementation("com.github.skydoves:landscapist-glide:2.1.11")
|
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
|
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<SigningConfig>.pixelized get() = this.getByName("pixelized")
|
private val NamedDomainObjectContainer<SigningConfig>.pixelized get() = this.getByName("pixelized")
|
||||||
|
|
@ -8,8 +8,11 @@ import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import com.pixelized.rplexicon.ui.navigation.ScreenNavHost
|
import com.pixelized.rplexicon.ui.navigation.ScreenNavHost
|
||||||
|
|
@ -17,6 +20,8 @@ import com.pixelized.rplexicon.ui.theme.LexiconTheme
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
||||||
val LocalActivity = staticCompositionLocalOf<Activity> { error("Activity not available") }
|
val LocalActivity = staticCompositionLocalOf<Activity> { error("Activity not available") }
|
||||||
|
val LocalSnack =
|
||||||
|
staticCompositionLocalOf<SnackbarHostState> { error("SnackbarHostState not available") }
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
@ -27,7 +32,8 @@ class MainActivity : ComponentActivity() {
|
||||||
setContent {
|
setContent {
|
||||||
LexiconTheme {
|
LexiconTheme {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalActivity provides this
|
LocalActivity provides this,
|
||||||
|
LocalSnack provides remember { SnackbarHostState() }
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
content = { padding ->
|
content = { padding ->
|
||||||
|
|
@ -39,6 +45,9 @@ class MainActivity : ComponentActivity() {
|
||||||
) {
|
) {
|
||||||
ScreenNavHost()
|
ScreenNavHost()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
snackbarHost = {
|
||||||
|
SnackbarHost(hostState = LocalSnack.current)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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<Boolean> 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,32 +1,68 @@
|
||||||
package com.pixelized.rplexicon.repository
|
package com.pixelized.rplexicon.repository
|
||||||
|
|
||||||
import android.net.Uri
|
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.model.Lexicon
|
||||||
import com.pixelized.rplexicon.network.IGoogleSpreadSheet
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.withContext
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class LexiconRepository @Inject constructor(
|
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<List<Lexicon>>(emptyList())
|
private val _data = MutableStateFlow<List<Lexicon>>(emptyList())
|
||||||
val data: StateFlow<List<Lexicon>> get() = _data
|
val data: StateFlow<List<Lexicon>> get() = _data
|
||||||
|
|
||||||
init {
|
@Throws(ServiceNotReady::class, Exception::class)
|
||||||
scope.launch {
|
suspend fun fetchLexicon(): ValueRange? {
|
||||||
_data.emit(sample())
|
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<Lexicon> {
|
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<Lexicon> {
|
||||||
return listOf(
|
return listOf(
|
||||||
Lexicon(
|
Lexicon(
|
||||||
name = "Brulkhai",
|
name = "Brulkhai",
|
||||||
|
|
@ -76,5 +112,4 @@ class LexiconRepository @Inject constructor(
|
||||||
history = null,
|
history = null,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -42,7 +42,7 @@ fun AuthenticationScreen(
|
||||||
) {
|
) {
|
||||||
val screen = LocalScreenNavHost.current
|
val screen = LocalScreenNavHost.current
|
||||||
val activity = LocalActivity.current
|
val activity = LocalActivity.current
|
||||||
val state = viewModel.rememberAuthenticationState(activity = activity)
|
val state = viewModel.rememberAuthenticationState()
|
||||||
|
|
||||||
Surface {
|
Surface {
|
||||||
AuthenticationScreenContent(
|
AuthenticationScreenContent(
|
||||||
|
|
|
||||||
|
|
@ -16,51 +16,41 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import com.google.android.gms.auth.api.identity.GetSignInIntentRequest
|
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.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.R
|
||||||
|
import com.pixelized.rplexicon.repository.AuthenticationRepository
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class AuthenticationViewModel @Inject constructor(
|
class AuthenticationViewModel @Inject constructor(
|
||||||
application: Application,
|
application: Application,
|
||||||
|
private val repository: AuthenticationRepository,
|
||||||
) : AndroidViewModel(application) {
|
) : AndroidViewModel(application) {
|
||||||
|
|
||||||
private val context: Context get() = getApplication()
|
private val context: Context get() = getApplication()
|
||||||
private var launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>? = null
|
private var launcher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>? = null
|
||||||
|
|
||||||
private val account: GoogleSignInAccount? by lazy {
|
|
||||||
GoogleSignIn.getLastSignedInAccount(application)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val lastGoogleToken: String? by lazy {
|
|
||||||
GoogleSignIn.getLastSignedInAccount(application)?.idToken
|
|
||||||
}
|
|
||||||
|
|
||||||
private val state = mutableStateOf(
|
private val state = mutableStateOf(
|
||||||
when (account) {
|
when (repository.isAuthenticated.value) {
|
||||||
null -> Authentication.Initial
|
true -> Authentication.Success
|
||||||
else -> Authentication.Success
|
else -> Authentication.Initial
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberAuthenticationState(activity: Activity): State<Authentication> {
|
fun rememberAuthenticationState(): State<Authentication> {
|
||||||
launcher = rememberLauncherForActivityResult(
|
launcher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.StartIntentSenderForResult(),
|
contract = ActivityResultContracts.StartIntentSenderForResult(),
|
||||||
onResult = {
|
onResult = {
|
||||||
if (it.resultCode == Activity.RESULT_OK) {
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
val credential = Identity
|
|
||||||
.getSignInClient(activity)
|
|
||||||
.getSignInCredentialFromIntent(it.data)
|
|
||||||
state.value = Authentication.Success
|
state.value = Authentication.Success
|
||||||
|
repository.updateAuthenticationState()
|
||||||
} else {
|
} else {
|
||||||
state.value = Authentication.Failure
|
state.value = Authentication.Failure
|
||||||
|
repository.updateAuthenticationState()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,10 +69,12 @@ class AuthenticationViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
} catch (e: SendIntentException) {
|
} catch (e: SendIntentException) {
|
||||||
state.value = Authentication.Failure
|
state.value = Authentication.Failure
|
||||||
|
repository.updateAuthenticationState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.addOnFailureListener { e ->
|
.addOnFailureListener { e ->
|
||||||
state.value = Authentication.Failure
|
state.value = Authentication.Failure
|
||||||
|
repository.updateAuthenticationState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.foundation.ScrollState
|
import androidx.compose.foundation.ScrollState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
|
@ -77,7 +78,7 @@ fun CharacterDetailScreen(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
item = viewModel.character,
|
item = viewModel.character,
|
||||||
onBack = { },
|
onBack = { },
|
||||||
onItem = { },
|
onImage = { },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -89,7 +90,7 @@ private fun CharacterDetailScreenContent(
|
||||||
state: ScrollState = rememberScrollState(),
|
state: ScrollState = rememberScrollState(),
|
||||||
item: State<CharacterDetailUio>,
|
item: State<CharacterDetailUio>,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onItem: () -> Unit,
|
onImage: (Uri) -> Unit,
|
||||||
) {
|
) {
|
||||||
val colorScheme = MaterialTheme.colorScheme
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val typography = MaterialTheme.typography
|
val typography = MaterialTheme.typography
|
||||||
|
|
@ -227,7 +228,9 @@ private fun CharacterDetailScreenContent(
|
||||||
) {
|
) {
|
||||||
items(items = item.value.portrait) {
|
items(items = item.value.portrait) {
|
||||||
GlideImage(
|
GlideImage(
|
||||||
modifier = Modifier.height(320.dp),
|
modifier = Modifier
|
||||||
|
.clickable { onImage(it) }
|
||||||
|
.height(320.dp),
|
||||||
imageModel = { it },
|
imageModel = { it },
|
||||||
imageOptions = ImageOptions(
|
imageOptions = ImageOptions(
|
||||||
contentScale = ContentScale.FillHeight
|
contentScale = ContentScale.FillHeight
|
||||||
|
|
@ -239,7 +242,6 @@ private fun CharacterDetailScreenContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -299,7 +301,7 @@ private fun CharacterDetailScreenContentPreview() {
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
item = character,
|
item = character,
|
||||||
onBack = { },
|
onBack = { },
|
||||||
onItem = { },
|
onImage = { },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
package com.pixelized.rplexicon.ui.screens.lexicon
|
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_NO
|
||||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
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.clickable
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
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.foundation.lazy.items
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import com.pixelized.rplexicon.LocalSnack
|
||||||
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
|
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
|
||||||
import com.pixelized.rplexicon.ui.navigation.navigateToCharacterDetail
|
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 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
|
@Composable
|
||||||
fun LexiconScreen(
|
fun LexiconScreen(
|
||||||
|
|
@ -35,6 +56,13 @@ fun LexiconScreen(
|
||||||
screen.navigateToCharacterDetail(id = "")
|
screen.navigateToCharacterDetail(id = "")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
HandleError(
|
||||||
|
errors = viewModel.error,
|
||||||
|
onLexiconPermissionGranted = {
|
||||||
|
viewModel.fetchLexicon()
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,6 +88,34 @@ private fun LexiconScreenContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HandleError(
|
||||||
|
errors: SharedFlow<LexiconErrorUio>,
|
||||||
|
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
|
@Composable
|
||||||
@Preview(uiMode = UI_MODE_NIGHT_NO)
|
@Preview(uiMode = UI_MODE_NIGHT_NO)
|
||||||
@Preview(uiMode = UI_MODE_NIGHT_YES)
|
@Preview(uiMode = UI_MODE_NIGHT_YES)
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
package com.pixelized.rplexicon.ui.screens.lexicon
|
package com.pixelized.rplexicon.ui.screens.lexicon
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.runtime.State
|
import androidx.compose.runtime.State
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.model.Lexicon
|
||||||
import com.pixelized.rplexicon.repository.LexiconRepository
|
import com.pixelized.rplexicon.repository.LexiconRepository
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
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 kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -15,12 +20,15 @@ class LexiconViewModel @Inject constructor(
|
||||||
private val repository: LexiconRepository,
|
private val repository: LexiconRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
// TODO : link it to a paginated DataSource
|
|
||||||
private val _items = mutableStateOf<List<LexiconItemUio>>(emptyList())
|
private val _items = mutableStateOf<List<LexiconItemUio>>(emptyList())
|
||||||
val items: State<List<LexiconItemUio>> get() = _items
|
val items: State<List<LexiconItemUio>> get() = _items
|
||||||
|
|
||||||
|
private val _error = MutableSharedFlow<LexiconErrorUio>()
|
||||||
|
val error: SharedFlow<LexiconErrorUio> get() = _error
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
launch {
|
||||||
repository.data.collect { items ->
|
repository.data.collect { items ->
|
||||||
_items.value = items.mapNotNull { item ->
|
_items.value = items.mapNotNull { item ->
|
||||||
item.name?.let {
|
item.name?.let {
|
||||||
|
|
@ -38,5 +46,28 @@ class LexiconViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue