Link the spreadsheet API to the dataSource.
This commit is contained in:
parent
6cfd673335
commit
6167999001
10 changed files with 164 additions and 125 deletions
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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<Boolean> get() = _isAuthenticated
|
||||
|
||||
private val account: GoogleSignInAccount?
|
||||
get() = GoogleSignIn.getLastSignedInAccount(context)
|
||||
private val signInCredential = mutableStateOf<SignInCredential?>(null)
|
||||
val isAuthenticated: State<Boolean> = 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<List<Lexicon>>(emptyList())
|
||||
val data: StateFlow<List<Lexicon>> 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<Lexicon> = sheet?.mapIndexedNotNull { index, row ->
|
||||
if (index == 0) {
|
||||
checkSheetStructure(firstRow = row)
|
||||
null
|
||||
} else {
|
||||
parseCharacterRow(index = index - 1, row = row as? List<Any>?)
|
||||
}
|
||||
} ?: 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<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?
|
||||
|
||||
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<Any>?.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<Lexicon> {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 = { },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<CharacterDetailUio> = mutableStateOf(init())
|
||||
|
||||
val character: State<CharacterDetailUio> = 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,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue