Refactor application loading + Paging on main Page
This commit is contained in:
parent
45f5e9023e
commit
1e58752008
26 changed files with 596 additions and 423 deletions
|
|
@ -109,7 +109,7 @@ dependencies {
|
|||
kapt "androidx.room:room-compiler:2.3.0"
|
||||
|
||||
// Paging
|
||||
implementation "androidx.paging:paging-runtime-ktx:3.0.0"
|
||||
implementation "androidx.paging:paging-compose:1.0.0-alpha08"
|
||||
|
||||
// Test
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@
|
|||
|
||||
<application
|
||||
android:name=".BibLibApplication"
|
||||
android:allowBackup="true"
|
||||
android:fullBackupOnly="true"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.pixelized.biblib.database.dao
|
||||
|
||||
import androidx.paging.DataSource
|
||||
import androidx.room.*
|
||||
import com.pixelized.biblib.database.data.BookDbo
|
||||
import com.pixelized.biblib.database.relation.BookRelation
|
||||
|
|
@ -10,6 +11,10 @@ interface BookDao {
|
|||
@Query("SELECT * FROM ${BookDbo.TABLE}")
|
||||
fun getAll(): List<BookRelation>
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM ${BookDbo.TABLE}")
|
||||
fun getBook(): DataSource.Factory<Int, BookRelation>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(vararg books: BookDbo)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import com.pixelized.biblib.database.data.*
|
|||
|
||||
data class BookRelation(
|
||||
@Embedded
|
||||
val bookDbo: BookDbo,
|
||||
val book: BookDbo,
|
||||
@Relation(
|
||||
parentColumn = BookDbo.ID,
|
||||
entityColumn = AuthorDbo.ID,
|
||||
|
|
@ -21,15 +21,15 @@ data class BookRelation(
|
|||
entityColumn = GenreDbo.ID,
|
||||
associateBy = Junction(BookGenreCrossRef::class)
|
||||
)
|
||||
val genres: List<GenreDbo>,
|
||||
val genres: List<GenreDbo>?,
|
||||
@Relation(
|
||||
parentColumn = BookDbo.LANGUAGE_ID,
|
||||
entityColumn = LanguageDbo.ID
|
||||
)
|
||||
val language: LanguageDbo,
|
||||
val language: LanguageDbo?,
|
||||
@Relation(
|
||||
parentColumn = BookDbo.SERIES_ID,
|
||||
entityColumn = SeriesDbo.ID
|
||||
)
|
||||
val series: SeriesDbo,
|
||||
val series: SeriesDbo?,
|
||||
)
|
||||
|
|
@ -18,13 +18,9 @@ class BibLibClient : IBibLibClient {
|
|||
|
||||
override val service: IBibLibWebServiceAPI = retrofit.create(IBibLibWebServiceAPI::class.java)
|
||||
|
||||
// endregion
|
||||
///////////////////////////////////
|
||||
// region BibLib webservice Auth
|
||||
|
||||
override fun updateBearerToken(token: String) {
|
||||
interceptor.token = token
|
||||
}
|
||||
override var token: String?
|
||||
get() = interceptor.token
|
||||
set(value) { interceptor.token = value }
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ interface IBibLibClient {
|
|||
|
||||
val service: IBibLibWebServiceAPI
|
||||
|
||||
fun updateBearerToken(token: String)
|
||||
var token: String?
|
||||
|
||||
companion object {
|
||||
const val BASE_URL = "https://bib.bibulle.fr"
|
||||
|
|
|
|||
|
|
@ -1,16 +1,27 @@
|
|||
package com.pixelized.biblib.repository.book
|
||||
|
||||
|
||||
import android.util.Log
|
||||
import androidx.paging.DataSource
|
||||
import com.pixelized.biblib.database.BibLibDatabase
|
||||
import com.pixelized.biblib.database.crossref.BookAuthorCrossRef
|
||||
import com.pixelized.biblib.database.crossref.BookGenreCrossRef
|
||||
import com.pixelized.biblib.database.data.*
|
||||
import com.pixelized.biblib.database.relation.BookRelation
|
||||
import com.pixelized.biblib.model.*
|
||||
import com.pixelized.biblib.utils.injection.inject
|
||||
|
||||
class BookRepository : IBookRepository {
|
||||
val database: BibLibDatabase by inject()
|
||||
|
||||
override fun update(data: List<Book>) {
|
||||
override fun getAll(): List<Book> =
|
||||
database.bookDao().getAll().map { it.toBook() }
|
||||
|
||||
override fun getBook(): DataSource.Factory<Int, Book> =
|
||||
database.bookDao().getBook().map { it.toBook() }
|
||||
|
||||
override suspend fun update(data: List<Book>) {
|
||||
Log.d("pouet", "BookRepository#update(): $data")
|
||||
val authors = mutableSetOf<AuthorDbo>()
|
||||
val genres = mutableSetOf<GenreDbo>()
|
||||
val series = mutableSetOf<SeriesDbo>()
|
||||
|
|
@ -83,4 +94,42 @@ class BookRepository : IBookRepository {
|
|||
synopsis = synopsis,
|
||||
isNew = isNew,
|
||||
)
|
||||
|
||||
private fun BookRelation.toBook(): Book = Book(
|
||||
id = book.id,
|
||||
title = book.title,
|
||||
sort = book.sort,
|
||||
author = authors.map { it.toAuthor() },
|
||||
haveCover = book.haveCover,
|
||||
releaseDate = book.releaseDate,
|
||||
language = language?.toLanguage(),
|
||||
rating = book.rating,
|
||||
genre = genres?.map { it.toGenre() },
|
||||
series = series?.toSeries(),
|
||||
synopsis = book.synopsis,
|
||||
isNew = book.isNew,
|
||||
)
|
||||
|
||||
private fun AuthorDbo.toAuthor() = Author(
|
||||
id = id,
|
||||
name = name,
|
||||
sort = sort,
|
||||
)
|
||||
|
||||
private fun LanguageDbo.toLanguage() = Language(
|
||||
id = id,
|
||||
code = code,
|
||||
)
|
||||
|
||||
private fun GenreDbo.toGenre() = Genre(
|
||||
id = id,
|
||||
name = name,
|
||||
)
|
||||
|
||||
private fun SeriesDbo.toSeries() = Series(
|
||||
id = id,
|
||||
name = name,
|
||||
sort = sort,
|
||||
index = index,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,7 +1,13 @@
|
|||
package com.pixelized.biblib.repository.book
|
||||
|
||||
import androidx.paging.DataSource
|
||||
import com.pixelized.biblib.model.Book
|
||||
|
||||
interface IBookRepository {
|
||||
fun update(data: List<Book>)
|
||||
|
||||
fun getAll(): List<Book>
|
||||
|
||||
fun getBook(): DataSource.Factory<Int, Book>
|
||||
|
||||
suspend fun update(data: List<Book>)
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ import com.pixelized.biblib.ui.composable.screen.LoginScreenComposable
|
|||
import com.pixelized.biblib.ui.composable.screen.MainScreenComposable
|
||||
import com.pixelized.biblib.ui.composable.screen.SplashScreenComposable
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation.Screen
|
||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel.Screen
|
||||
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ fun CrossFadeOverlay(
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
.alpha(.25f)
|
||||
.alpha(.75f)
|
||||
.background(Color.Black)
|
||||
)
|
||||
// Overlay content.
|
||||
|
|
|
|||
|
|
@ -1,49 +1,56 @@
|
|||
package com.pixelized.biblib.ui.composable.pages
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.items
|
||||
import com.pixelized.biblib.ui.composable.items.BookThumbnailComposable
|
||||
import com.pixelized.biblib.ui.data.BookThumbnailUio
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation
|
||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation.Page.Detail
|
||||
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
|
||||
import com.pixelized.biblib.utils.mock.BookMock
|
||||
import com.pixelized.biblib.utils.mock.BookThumbnailMock
|
||||
import com.pixelized.biblib.ui.viewmodel.book.IBooksViewModel
|
||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel
|
||||
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun HomePageComposablePreview() {
|
||||
BibLibTheme {
|
||||
val navigation = NavigationViewModel()
|
||||
HomePageComposable(navigation)
|
||||
val navigation = INavigationViewModel.Mock()
|
||||
val books = IBooksViewModel.Mock()
|
||||
HomePageComposable(navigation, books)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HomePageComposable(navigation: INavigation) {
|
||||
val mock = BookThumbnailMock()
|
||||
fun HomePageComposable(
|
||||
navigationViewModel: INavigationViewModel,
|
||||
booksViewModel: IBooksViewModel
|
||||
) {
|
||||
val lazyBooks: LazyPagingItems<BookThumbnailUio> = booksViewModel.books.collectAsLazyPagingItems()
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
items(mock.bookThumbnails) { thumbnail ->
|
||||
items(lazyBooks) { thumbnail ->
|
||||
BookThumbnailComposable(
|
||||
thumbnail = thumbnail,
|
||||
thumbnail = thumbnail!!,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
) { item ->
|
||||
// TODO:
|
||||
val bookMock = BookMock().let { it.books[item.id] ?: it.book }
|
||||
navigation.navigateTo(Detail(bookMock))
|
||||
// val bookMock = BookMock().let { it.books[item.id] ?: it.book }
|
||||
// navigation.navigateTo(INavigation.Page.Detail(bookMock))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,10 +44,12 @@ import com.pixelized.biblib.ui.composable.items.dialog.LoadingCard
|
|||
import com.pixelized.biblib.ui.composable.items.dialog.SuccessCard
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
import com.pixelized.biblib.ui.viewmodel.authentication.AuthenticationViewModel
|
||||
import com.pixelized.biblib.ui.viewmodel.authentication.IAuthentication
|
||||
import com.pixelized.biblib.ui.viewmodel.initialisation.IInitialisation
|
||||
import com.pixelized.biblib.ui.viewmodel.initialisation.InitialisationViewModel
|
||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation
|
||||
import com.pixelized.biblib.ui.viewmodel.authentication.IAuthenticationViewModel
|
||||
import com.pixelized.biblib.ui.viewmodel.book.BooksViewModel
|
||||
import com.pixelized.biblib.ui.viewmodel.book.IBooksViewModel
|
||||
import com.pixelized.biblib.ui.viewmodel.credential.CredentialViewModel
|
||||
import com.pixelized.biblib.ui.viewmodel.credential.ICredentialViewModel
|
||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel
|
||||
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
|
||||
|
||||
|
||||
|
|
@ -55,26 +57,31 @@ private const val LE_LOAD_BOOK = "LE_LOAD_BOOK"
|
|||
|
||||
@Composable
|
||||
fun LoginScreenComposable(
|
||||
navigation: INavigation = viewModel<NavigationViewModel>(),
|
||||
authentication: IAuthentication = viewModel<AuthenticationViewModel>(),
|
||||
initialisation: IInitialisation = viewModel<InitialisationViewModel>()
|
||||
navigationViewModel: INavigationViewModel = viewModel<NavigationViewModel>(),
|
||||
credentialViewModel: ICredentialViewModel = viewModel<CredentialViewModel>(),
|
||||
authenticationViewModel: IAuthenticationViewModel = viewModel<AuthenticationViewModel>(),
|
||||
bookViewModel: IBooksViewModel = viewModel<BooksViewModel>()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
) {
|
||||
authentication.PrepareLoginWithGoogle()
|
||||
LoginScreenNavigationComposable(navigation, authentication, initialisation)
|
||||
|
||||
LoginScreenContentComposable(authentication)
|
||||
LoginScreenDialogComposable(authentication, initialisation)
|
||||
authenticationViewModel.PrepareLoginWithGoogle()
|
||||
LoginScreenNavigationComposable(
|
||||
navigationViewModel,
|
||||
authenticationViewModel,
|
||||
bookViewModel
|
||||
)
|
||||
LoginScreenContentComposable(credentialViewModel, authenticationViewModel)
|
||||
LoginScreenDialogComposable(authenticationViewModel, bookViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoginScreenContentComposable(
|
||||
authentication: IAuthentication,
|
||||
credentialViewModel: ICredentialViewModel,
|
||||
authenticationViewModel: IAuthenticationViewModel,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
|
@ -86,45 +93,47 @@ private fun LoginScreenContentComposable(
|
|||
Spacer(modifier = Modifier.weight(1f))
|
||||
Title()
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Form(authentication)
|
||||
Form(credentialViewModel, authenticationViewModel)
|
||||
Spacer(modifier = Modifier.weight(2f))
|
||||
SignIn(authentication)
|
||||
SignIn(authenticationViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
private fun LoginScreenDialogComposable(
|
||||
authentication: IAuthentication,
|
||||
initialisation: IInitialisation,
|
||||
authenticationViewModel: IAuthenticationViewModel,
|
||||
booksViewModel: IBooksViewModel,
|
||||
) {
|
||||
val authenticationState = authentication.state.observeAsState()
|
||||
val bookLoadingState = initialisation.state.observeAsState()
|
||||
val authState = authenticationViewModel.state.observeAsState()
|
||||
val bookState = booksViewModel.state.observeAsState()
|
||||
|
||||
val visible = authState.value is IAuthenticationViewModel.State.Error
|
||||
|| bookState.value is IBooksViewModel.State.Error
|
||||
|
||||
CrossFadeOverlay(
|
||||
modifier = Modifier.clickable {
|
||||
if (authenticationState.value is IAuthentication.State.Error) {
|
||||
authentication.clearState()
|
||||
if (visible) {
|
||||
authenticationViewModel.clearState()
|
||||
}
|
||||
},
|
||||
visible = (authenticationState.value is IAuthentication.State.Initial
|
||||
&& bookLoadingState.value is IInitialisation.State.Initial).not()
|
||||
visible = visible
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
visible = authenticationState.value is IAuthentication.State.Error,
|
||||
visible = authState.value is IAuthenticationViewModel.State.Error,
|
||||
initiallyVisible = false,
|
||||
enter = expandVertically(Alignment.CenterVertically),
|
||||
exit = shrinkVertically(Alignment.CenterVertically),
|
||||
) {
|
||||
ErrorCard(
|
||||
message = stringResource(id = R.string.error_generic),
|
||||
exception = (authenticationState.value as? IAuthentication.State.Error)?.exception
|
||||
message = stringResource(id = R.string.error_authentication),
|
||||
exception = (authState.value as? IAuthenticationViewModel.State.Error)?.exception
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
visible = authenticationState.value is IAuthentication.State.Loading,
|
||||
visible = authState.value is IAuthenticationViewModel.State.Loading,
|
||||
initiallyVisible = false,
|
||||
enter = expandVertically(Alignment.CenterVertically),
|
||||
exit = shrinkVertically(Alignment.CenterVertically),
|
||||
|
|
@ -135,7 +144,8 @@ private fun LoginScreenDialogComposable(
|
|||
}
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
visible = authenticationState.value is IAuthentication.State.Connect,
|
||||
visible = (authState.value as? IAuthenticationViewModel.State.Finished)?.logged
|
||||
?: false,
|
||||
initiallyVisible = false,
|
||||
enter = expandVertically(Alignment.CenterVertically),
|
||||
exit = shrinkVertically(Alignment.CenterVertically),
|
||||
|
|
@ -146,19 +156,19 @@ private fun LoginScreenDialogComposable(
|
|||
}
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
visible = bookLoadingState.value is IInitialisation.State.Error,
|
||||
visible = bookState.value is IBooksViewModel.State.Error,
|
||||
initiallyVisible = false,
|
||||
enter = expandVertically(Alignment.CenterVertically),
|
||||
exit = shrinkVertically(Alignment.CenterVertically),
|
||||
) {
|
||||
ErrorCard(
|
||||
message = stringResource(id = R.string.error_generic),
|
||||
exception = (bookLoadingState.value as? IInitialisation.State.Error)?.exception
|
||||
message = stringResource(id = R.string.error_book),
|
||||
exception = (bookState.value as? IBooksViewModel.State.Error)?.exception
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
visible = bookLoadingState.value is IInitialisation.State.Loading,
|
||||
visible = bookState.value is IBooksViewModel.State.Loading,
|
||||
initiallyVisible = false,
|
||||
enter = expandVertically(Alignment.CenterVertically),
|
||||
exit = shrinkVertically(Alignment.CenterVertically),
|
||||
|
|
@ -169,7 +179,7 @@ private fun LoginScreenDialogComposable(
|
|||
}
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
visible = bookLoadingState.value is IInitialisation.State.Finished,
|
||||
visible = bookState.value is IBooksViewModel.State.Finished,
|
||||
initiallyVisible = false,
|
||||
enter = expandVertically(Alignment.CenterVertically),
|
||||
exit = shrinkVertically(Alignment.CenterVertically),
|
||||
|
|
@ -183,22 +193,22 @@ private fun LoginScreenDialogComposable(
|
|||
|
||||
@Composable
|
||||
private fun LoginScreenNavigationComposable(
|
||||
navigation: INavigation,
|
||||
authentication: IAuthentication,
|
||||
initialisation: IInitialisation,
|
||||
navigationViewModel: INavigationViewModel,
|
||||
authenticationViewModel: IAuthenticationViewModel,
|
||||
bookViewModel: IBooksViewModel,
|
||||
) {
|
||||
val authenticationState = authentication.state.observeAsState()
|
||||
val bookLoadingState = initialisation.state.observeAsState()
|
||||
|
||||
if (authenticationState.value == IAuthentication.State.Connect) {
|
||||
LaunchedEffect(LE_LOAD_BOOK) {
|
||||
initialisation.loadBook()
|
||||
val authenticationState = authenticationViewModel.state.observeAsState()
|
||||
(authenticationState.value as? IAuthenticationViewModel.State.Finished)?.let {
|
||||
if (it.logged) {
|
||||
LaunchedEffect(LE_LOAD_BOOK) {
|
||||
bookViewModel.updateBooks()
|
||||
}
|
||||
val bookLoadingState = bookViewModel.state.observeAsState()
|
||||
if (bookLoadingState.value is IBooksViewModel.State.Finished) {
|
||||
navigationViewModel.navigateTo(INavigationViewModel.Screen.MainScreen)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bookLoadingState.value is IInitialisation.State.Finished) {
|
||||
navigation.navigateTo(INavigation.Screen.MainScreen)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -215,12 +225,13 @@ private fun ColumnScope.Title() {
|
|||
|
||||
@Composable
|
||||
private fun ColumnScope.Form(
|
||||
authentication: IAuthentication,
|
||||
credentialViewModel: ICredentialViewModel,
|
||||
authenticationViewModel: IAuthenticationViewModel,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val localFocus = LocalFocusManager.current
|
||||
LoginField(
|
||||
authentication = authentication,
|
||||
credentialViewModel = credentialViewModel,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp),
|
||||
|
|
@ -228,7 +239,7 @@ private fun ColumnScope.Form(
|
|||
keyboardActions = KeyboardActions { focusRequester.requestFocus() }
|
||||
)
|
||||
PasswordField(
|
||||
authentication = authentication,
|
||||
credentialViewModel = credentialViewModel,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp)
|
||||
|
|
@ -237,7 +248,7 @@ private fun ColumnScope.Form(
|
|||
keyboardActions = KeyboardActions { localFocus.clearFocus() }
|
||||
)
|
||||
CredentialRemember(
|
||||
authentication = authentication,
|
||||
credentialViewModel = credentialViewModel,
|
||||
modifier = Modifier
|
||||
.height(48.dp)
|
||||
.padding(bottom = 16.dp)
|
||||
|
|
@ -257,7 +268,12 @@ private fun ColumnScope.Form(
|
|||
Text(text = stringResource(id = R.string.action_register))
|
||||
}
|
||||
Button(onClick = {
|
||||
authentication.login()
|
||||
val login = credentialViewModel.login.value
|
||||
val password = credentialViewModel.password.value
|
||||
val rememberCredential = credentialViewModel.rememberCredential.value
|
||||
if (login != null && password != null && rememberCredential != null) {
|
||||
authenticationViewModel.login(login, password, rememberCredential)
|
||||
}
|
||||
}) {
|
||||
Text(text = stringResource(id = R.string.action_login))
|
||||
}
|
||||
|
|
@ -266,13 +282,13 @@ private fun ColumnScope.Form(
|
|||
|
||||
@Composable
|
||||
private fun SignIn(
|
||||
authentication: IAuthentication,
|
||||
authenticationViewModel: IAuthenticationViewModel,
|
||||
) {
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = outlinedButtonColors(),
|
||||
onClick = {
|
||||
authentication.loginWithGoogle()
|
||||
authenticationViewModel.loginWithGoogle()
|
||||
}) {
|
||||
Image(
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
|
|
@ -284,16 +300,16 @@ private fun SignIn(
|
|||
|
||||
@Composable
|
||||
private fun LoginField(
|
||||
authentication: IAuthentication,
|
||||
credentialViewModel: ICredentialViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions(),
|
||||
) {
|
||||
val login: State<String?> = authentication.login.observeAsState()
|
||||
val login: State<String?> = credentialViewModel.login.observeAsState()
|
||||
TextField(
|
||||
modifier = modifier,
|
||||
value = login.value ?: "",
|
||||
onValueChange = { authentication.updateLoginField(it) },
|
||||
onValueChange = { credentialViewModel.updateLoginField(it) },
|
||||
label = { Text(text = stringResource(id = R.string.authentication_login)) },
|
||||
colors = outlinedTextFieldColors(),
|
||||
maxLines = 1,
|
||||
|
|
@ -305,17 +321,17 @@ private fun LoginField(
|
|||
|
||||
@Composable
|
||||
private fun PasswordField(
|
||||
authentication: IAuthentication,
|
||||
credentialViewModel: ICredentialViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions(),
|
||||
) {
|
||||
val password = authentication.password.observeAsState()
|
||||
val password = credentialViewModel.password.observeAsState()
|
||||
var passwordVisibility by remember { mutableStateOf(false) }
|
||||
TextField(
|
||||
modifier = modifier,
|
||||
value = password.value ?: "",
|
||||
onValueChange = { authentication.updatePasswordField(it) },
|
||||
onValueChange = { credentialViewModel.updatePasswordField(it) },
|
||||
label = { Text(text = stringResource(id = R.string.authentication_password)) },
|
||||
colors = outlinedTextFieldColors(),
|
||||
maxLines = 1,
|
||||
|
|
@ -336,12 +352,12 @@ private fun PasswordField(
|
|||
|
||||
@Composable
|
||||
private fun CredentialRemember(
|
||||
authentication: IAuthentication,
|
||||
credentialViewModel: ICredentialViewModel,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val credential = authentication.rememberCredential.observeAsState()
|
||||
val credential = credentialViewModel.rememberCredential.observeAsState()
|
||||
Row(modifier = modifier.clickable {
|
||||
authentication.updateRememberCredential(
|
||||
credentialViewModel.updateRememberCredential(
|
||||
rememberCredential = credential.value?.not() ?: false
|
||||
)
|
||||
}) {
|
||||
|
|
@ -363,9 +379,15 @@ private fun CredentialRemember(
|
|||
@Composable
|
||||
fun LoginScreenComposablePreview() {
|
||||
BibLibTheme {
|
||||
val navigationViewModel = INavigation.Mock()
|
||||
val authenticationViewModel = IAuthentication.Mock()
|
||||
val initialisation = IInitialisation.Mock()
|
||||
LoginScreenComposable(navigationViewModel, authenticationViewModel, initialisation)
|
||||
val navigationViewModel = INavigationViewModel.Mock()
|
||||
val credentialViewModel = ICredentialViewModel.Mock()
|
||||
val authenticationViewModel = IAuthenticationViewModel.Mock()
|
||||
val bookViewModel = IBooksViewModel.Mock()
|
||||
LoginScreenComposable(
|
||||
navigationViewModel,
|
||||
credentialViewModel,
|
||||
authenticationViewModel,
|
||||
bookViewModel,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +1,30 @@
|
|||
package com.pixelized.biblib.ui.composable.screen
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.sharp.ArrowBack
|
||||
import androidx.compose.material.icons.sharp.LocalLibrary
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.pixelized.biblib.R
|
||||
import com.pixelized.biblib.ui.composable.pages.DetailPageComposable
|
||||
import com.pixelized.biblib.ui.composable.pages.HomePageComposable
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation
|
||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation.Page
|
||||
import com.pixelized.biblib.ui.viewmodel.book.BooksViewModel
|
||||
import com.pixelized.biblib.ui.viewmodel.book.IBooksViewModel
|
||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel
|
||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel.Page
|
||||
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun ToolbarComposableDarkPreview() {
|
||||
BibLibTheme(darkTheme = false) {
|
||||
ToolbarComposable(navigation = INavigation.Mock())
|
||||
ToolbarComposable(navigationViewModel = INavigationViewModel.Mock())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -34,7 +32,7 @@ fun ToolbarComposableDarkPreview() {
|
|||
@Composable
|
||||
fun ToolbarComposableLightPreview() {
|
||||
BibLibTheme(darkTheme = true) {
|
||||
ToolbarComposable(navigation = INavigation.Mock())
|
||||
ToolbarComposable(navigationViewModel = INavigationViewModel.Mock())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -42,23 +40,27 @@ fun ToolbarComposableLightPreview() {
|
|||
@Composable
|
||||
fun MainScreenComposablePreview() {
|
||||
BibLibTheme {
|
||||
MainScreenComposable(INavigation.Mock(page = Page.HomePage))
|
||||
MainScreenComposable(
|
||||
INavigationViewModel.Mock(page = Page.HomePage),
|
||||
IBooksViewModel.Mock()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun MainScreenComposable(
|
||||
navigation: INavigation = viewModel<NavigationViewModel>()
|
||||
navigationViewModel: INavigationViewModel = viewModel<NavigationViewModel>(),
|
||||
booksViewModel: IBooksViewModel = viewModel<BooksViewModel>(),
|
||||
) {
|
||||
val page by navigation.page.observeAsState()
|
||||
val page by navigationViewModel.page.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = "MainScreen", block = {
|
||||
navigation.navigateTo(Page.HomePage)
|
||||
navigationViewModel.navigateTo(Page.HomePage)
|
||||
})
|
||||
|
||||
Scaffold(
|
||||
topBar = { ToolbarComposable(navigation) },
|
||||
topBar = { ToolbarComposable(navigationViewModel) },
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = page is Page.HomePage,
|
||||
|
|
@ -66,7 +68,7 @@ fun MainScreenComposable(
|
|||
enter = slideInHorizontally(initialOffsetX = { width -> -width }),
|
||||
exit = slideOutHorizontally(targetOffsetX = { width -> -width }),
|
||||
) {
|
||||
HomePageComposable(navigation)
|
||||
HomePageComposable(navigationViewModel, booksViewModel)
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = page is Page.Detail,
|
||||
|
|
@ -83,16 +85,16 @@ fun MainScreenComposable(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun ToolbarComposable(navigation: INavigation) {
|
||||
fun ToolbarComposable(navigationViewModel: INavigationViewModel) {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(id = R.string.app_name)) },
|
||||
navigationIcon = { NavigationIcon(navigation) }
|
||||
navigationIcon = { NavigationIcon(navigationViewModel) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NavigationIcon(navigation: INavigation) {
|
||||
val page: Page? by navigation.page.observeAsState()
|
||||
fun NavigationIcon(navigationViewModel: INavigationViewModel) {
|
||||
val page: Page? by navigationViewModel.page.observeAsState()
|
||||
|
||||
Crossfade(targetState = page) {
|
||||
when (it) {
|
||||
|
|
@ -103,7 +105,7 @@ fun NavigationIcon(navigation: INavigation) {
|
|||
)
|
||||
}
|
||||
else -> IconButton(onClick = {
|
||||
navigation.navigateBack()
|
||||
navigationViewModel.navigateBack()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Sharp.ArrowBack,
|
||||
|
|
|
|||
|
|
@ -6,9 +6,7 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -21,53 +19,93 @@ import com.pixelized.biblib.R
|
|||
import com.pixelized.biblib.ui.composable.items.dialog.CrossFadeOverlay
|
||||
import com.pixelized.biblib.ui.composable.items.dialog.ErrorCard
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
import com.pixelized.biblib.ui.viewmodel.initialisation.IInitialisation
|
||||
import com.pixelized.biblib.ui.viewmodel.initialisation.InitialisationViewModel
|
||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation
|
||||
import com.pixelized.biblib.ui.viewmodel.authentication.AuthenticationViewModel
|
||||
import com.pixelized.biblib.ui.viewmodel.authentication.IAuthenticationViewModel
|
||||
import com.pixelized.biblib.ui.viewmodel.book.BooksViewModel
|
||||
import com.pixelized.biblib.ui.viewmodel.book.IBooksViewModel
|
||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel
|
||||
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import java.util.*
|
||||
|
||||
private const val ANIMATION_DURATION = 1000
|
||||
private const val LAUNCH_EFFECT_LOAD_APPLICATION = "LoadApplication"
|
||||
private const val LAUNCH_EFFECT_AUTHENTICATION = "LAUNCH_EFFECT_AUTHENTICATION"
|
||||
private const val LAUNCH_EFFECT_BOOK = "LAUNCH_EFFECT_BOOK"
|
||||
private const val LAUNCH_EFFECT_NAVIGATION = "LAUNCH_EFFECT_NAVIGATION"
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun SplashScreenComposablePreview() {
|
||||
BibLibTheme {
|
||||
val initialisation = IInitialisation.Mock(IInitialisation.State.Loading)
|
||||
val navigation = INavigation.Mock()
|
||||
SplashScreenComposable(initialisation, navigation, true)
|
||||
val authenticationViewModel = IAuthenticationViewModel.Mock()
|
||||
val bookViewModel = IBooksViewModel.Mock()
|
||||
val navigation = INavigationViewModel.Mock()
|
||||
SplashScreenComposable(
|
||||
authenticationViewModel = authenticationViewModel,
|
||||
bookViewModel = bookViewModel,
|
||||
navigationViewModel = navigation,
|
||||
initiallyVisible = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun SplashScreenComposable(
|
||||
initialisation: IInitialisation = viewModel<InitialisationViewModel>(),
|
||||
navigation: INavigation = viewModel<NavigationViewModel>(),
|
||||
authenticationViewModel: IAuthenticationViewModel = viewModel<AuthenticationViewModel>(),
|
||||
bookViewModel: IBooksViewModel = viewModel<BooksViewModel>(),
|
||||
navigationViewModel: INavigationViewModel = viewModel<NavigationViewModel>(),
|
||||
initiallyVisible: Boolean = false,
|
||||
) {
|
||||
val state by initialisation.state.observeAsState()
|
||||
val authenticationState: IAuthenticationViewModel.State? by authenticationViewModel.state.observeAsState()
|
||||
val bookState by bookViewModel.state.observeAsState()
|
||||
val contentVisible = remember { mutableStateOf(true) }
|
||||
|
||||
LaunchedEffect(LAUNCH_EFFECT_LOAD_APPLICATION) {
|
||||
initialisation.loadApplication()
|
||||
// Content
|
||||
ContentComposable(
|
||||
visible = contentVisible.value,
|
||||
initiallyVisible = initiallyVisible
|
||||
)
|
||||
|
||||
// Dialog
|
||||
AuthenticationError(state = authenticationState as? IAuthenticationViewModel.State.Error)
|
||||
BookError(state = bookState as? IBooksViewModel.State.Error)
|
||||
|
||||
LaunchedEffect(LAUNCH_EFFECT_AUTHENTICATION) {
|
||||
delay(1500)
|
||||
authenticationViewModel.autoLogin()
|
||||
}
|
||||
|
||||
ContentComposable(state = state, initiallyVisible = initiallyVisible)
|
||||
DialogComposable(state = state)
|
||||
NavigationComposable(navigation = navigation, state = state)
|
||||
(authenticationState as? IAuthenticationViewModel.State.Finished)?.let {
|
||||
if (it.logged) {
|
||||
LaunchedEffect(LAUNCH_EFFECT_BOOK) {
|
||||
bookViewModel.updateBooks()
|
||||
}
|
||||
(bookState as? IBooksViewModel.State.Finished)?.let {
|
||||
LaunchedEffect(LAUNCH_EFFECT_NAVIGATION) {
|
||||
contentVisible.value = false
|
||||
delay(1000)
|
||||
navigationViewModel.navigateTo(INavigationViewModel.Screen.MainScreen)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LaunchedEffect(LAUNCH_EFFECT_NAVIGATION) {
|
||||
contentVisible.value = false
|
||||
delay(1000)
|
||||
navigationViewModel.navigateTo(INavigationViewModel.Screen.LoginScreen)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
private fun ContentComposable(
|
||||
state: IInitialisation.State?,
|
||||
duration: Int = ANIMATION_DURATION,
|
||||
visible: Boolean = false,
|
||||
initiallyVisible: Boolean = false,
|
||||
) {
|
||||
val typography = MaterialTheme.typography
|
||||
val visible = state !is IInitialisation.State.Finished
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
|
@ -143,10 +181,10 @@ private fun ContentComposable(
|
|||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
private fun DialogComposable(state: IInitialisation.State?) {
|
||||
private fun AuthenticationError(state: IAuthenticationViewModel.State.Error?) {
|
||||
CrossFadeOverlay(
|
||||
modifier = Modifier.clickable {},
|
||||
visible = state is IInitialisation.State.Error
|
||||
visible = state != null
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
|
|
@ -157,26 +195,30 @@ private fun DialogComposable(state: IInitialisation.State?) {
|
|||
) {
|
||||
ErrorCard(
|
||||
message = stringResource(id = R.string.error_generic),
|
||||
exception = (state as? IInitialisation.State.Error)?.exception
|
||||
exception = state?.exception
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
private fun NavigationComposable(
|
||||
navigation: INavigation,
|
||||
state: IInitialisation.State?,
|
||||
duration: Int = ANIMATION_DURATION,
|
||||
) {
|
||||
if (state is IInitialisation.State.Finished) {
|
||||
LaunchedEffect(key1 = "SplashScreen.navigateTo()") {
|
||||
delay(duration.toLong())
|
||||
if (state.needLogin) {
|
||||
navigation.navigateTo(INavigation.Screen.LoginScreen)
|
||||
} else {
|
||||
navigation.navigateTo(INavigation.Screen.MainScreen)
|
||||
}
|
||||
private fun BookError(state: IBooksViewModel.State.Error?) {
|
||||
CrossFadeOverlay(
|
||||
modifier = Modifier.clickable {},
|
||||
visible = state != null
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
visible = true,
|
||||
initiallyVisible = false,
|
||||
enter = expandVertically(Alignment.CenterVertically),
|
||||
exit = shrinkVertically(Alignment.CenterVertically),
|
||||
) {
|
||||
ErrorCard(
|
||||
message = stringResource(id = R.string.error_generic),
|
||||
exception = state?.exception
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.pixelized.biblib.ui.viewmodel.authentication
|
||||
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
|
|
@ -12,91 +13,60 @@ import androidx.lifecycle.viewModelScope
|
|||
import com.google.android.gms.auth.api.signin.GoogleSignIn
|
||||
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
|
||||
import com.google.android.gms.common.api.ApiException
|
||||
import com.pixelized.biblib.network.data.query.AuthLoginQuery
|
||||
import com.pixelized.biblib.network.client.IBibLibClient
|
||||
import com.pixelized.biblib.network.data.query.AuthLoginQuery
|
||||
import com.pixelized.biblib.repository.credential.ICredentialRepository
|
||||
import com.pixelized.biblib.repository.googleSignIn.IGoogleSingInRepository
|
||||
import com.pixelized.biblib.ui.viewmodel.authentication.IAuthentication.State
|
||||
import com.pixelized.biblib.ui.viewmodel.authentication.IAuthenticationViewModel.State
|
||||
import com.pixelized.biblib.utils.exception.MissingTokenException
|
||||
import com.pixelized.biblib.utils.injection.inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AuthenticationViewModel : ViewModel(), IAuthentication {
|
||||
|
||||
class AuthenticationViewModel : ViewModel(), IAuthenticationViewModel {
|
||||
private val credentialRepository: ICredentialRepository by inject()
|
||||
private val googleSignIn: IGoogleSingInRepository by inject()
|
||||
private val client: IBibLibClient by inject()
|
||||
|
||||
private var launcher: ActivityResultLauncher<Intent>? = null
|
||||
|
||||
private val _state = MutableLiveData<State>(State.Initial)
|
||||
override val state: LiveData<State> get() = _state
|
||||
|
||||
private val _login = MutableLiveData<String>()
|
||||
override val login: LiveData<String?> get() = _login
|
||||
|
||||
private val _password = MutableLiveData<String>()
|
||||
override val password: LiveData<String?> get() = _password
|
||||
|
||||
private val _rememberCredential = MutableLiveData<Boolean>()
|
||||
override val rememberCredential: LiveData<Boolean> get() = _rememberCredential
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
_login.value = credentialRepository.login
|
||||
_password.value = credentialRepository.password
|
||||
_rememberCredential.value = credentialRepository.rememberCredential
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateLoginField(login: String) {
|
||||
_login.postValue(login)
|
||||
}
|
||||
|
||||
override fun updatePasswordField(password: String) {
|
||||
_password.postValue(password)
|
||||
}
|
||||
|
||||
override fun updateRememberCredential(rememberCredential: Boolean) {
|
||||
viewModelScope.launch {
|
||||
_rememberCredential.postValue(rememberCredential)
|
||||
credentialRepository.rememberCredential = rememberCredential
|
||||
if (rememberCredential) {
|
||||
credentialRepository.login = login.value
|
||||
credentialRepository.password = password.value
|
||||
} else {
|
||||
credentialRepository.login = null
|
||||
credentialRepository.password = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearState() {
|
||||
_state.postValue(State.Initial)
|
||||
}
|
||||
|
||||
override fun login() {
|
||||
override fun autoLogin() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
// TODO : validation !
|
||||
if (rememberCredential.value == true) {
|
||||
credentialRepository.login = login.value
|
||||
credentialRepository.password = password.value
|
||||
_state.postValue(State.Initial)
|
||||
_state.postValue(State.Loading)
|
||||
try {
|
||||
val logged = autoLoginWithGoogle() || autologinWithCredential()
|
||||
_state.postValue(State.Finished(logged))
|
||||
} catch (exception: Exception) {
|
||||
_state.postValue(State.Error(exception))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun login(login: String, password: String, rememberCredential: Boolean) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
if (rememberCredential) {
|
||||
credentialRepository.login = login
|
||||
credentialRepository.password = password
|
||||
} else {
|
||||
credentialRepository.login = null
|
||||
credentialRepository.password = null
|
||||
}
|
||||
val query = AuthLoginQuery(
|
||||
username = login.value,
|
||||
password = password.value
|
||||
)
|
||||
val query = AuthLoginQuery(username = login, password = password)
|
||||
_state.postValue(State.Loading)
|
||||
try {
|
||||
val response = client.service.login(query)
|
||||
val idToken = response.token ?: throw MissingTokenException()
|
||||
client.updateBearerToken(idToken)
|
||||
_state.postValue(State.Connect)
|
||||
client.token = idToken
|
||||
_state.postValue(State.Finished(true))
|
||||
} catch (exception: Exception) {
|
||||
Log.e("AuthenticationViewModel", exception.message, exception)
|
||||
_state.postValue(State.Error(exception))
|
||||
}
|
||||
}
|
||||
|
|
@ -111,9 +81,10 @@ class AuthenticationViewModel : ViewModel(), IAuthentication {
|
|||
val task = GoogleSignIn.getSignedInAccountFromIntent(it.data)
|
||||
val account: GoogleSignInAccount? = task.getResult(ApiException::class.java)
|
||||
val idToken = account?.idToken ?: throw MissingTokenException()
|
||||
client.updateBearerToken(idToken)
|
||||
_state.postValue(State.Connect)
|
||||
client.token = idToken
|
||||
_state.postValue(State.Finished(true))
|
||||
} catch (exception: Exception) {
|
||||
Log.e("AuthenticationViewModel", exception.message, exception)
|
||||
_state.postValue(State.Error(exception))
|
||||
}
|
||||
}
|
||||
|
|
@ -123,4 +94,46 @@ class AuthenticationViewModel : ViewModel(), IAuthentication {
|
|||
_state.postValue(State.Loading)
|
||||
launcher?.launch(googleSignIn.client.signInIntent)
|
||||
}
|
||||
|
||||
private suspend fun autoLoginWithGoogle(): Boolean {
|
||||
val googleToken = googleSignIn.lastGoogleToken
|
||||
return if (googleToken != null) {
|
||||
try {
|
||||
client.service.loginWithGoogle(googleToken).let { response ->
|
||||
if (response.token != null) {
|
||||
client.token = response.token
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun autologinWithCredential(): Boolean {
|
||||
val login = credentialRepository.login
|
||||
val password = credentialRepository.password
|
||||
return if (login != null && password != null) {
|
||||
try {
|
||||
val query = AuthLoginQuery(login, password)
|
||||
client.service.login(query).let { response ->
|
||||
if (response.token != null) {
|
||||
client.token = response.token
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
package com.pixelized.biblib.ui.viewmodel.authentication
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
|
||||
interface IAuthentication {
|
||||
val state: LiveData<State>
|
||||
val login: LiveData<String?>
|
||||
val password: LiveData<String?>
|
||||
val rememberCredential: LiveData<Boolean>
|
||||
|
||||
fun updateLoginField(login: String)
|
||||
fun updatePasswordField(password: String)
|
||||
fun updateRememberCredential(rememberCredential: Boolean)
|
||||
fun clearState()
|
||||
|
||||
fun login()
|
||||
|
||||
@Composable
|
||||
fun PrepareLoginWithGoogle()
|
||||
fun loginWithGoogle()
|
||||
|
||||
sealed class State {
|
||||
object Initial : State()
|
||||
object Loading : State()
|
||||
object Connect : State()
|
||||
data class Error(val exception: Exception) : State()
|
||||
}
|
||||
|
||||
class Mock(state: State = State.Initial) : IAuthentication {
|
||||
override val state: LiveData<State> = MutableLiveData(state)
|
||||
override val login: LiveData<String?> = MutableLiveData("")
|
||||
override val password: LiveData<String?> = MutableLiveData("")
|
||||
override val rememberCredential: LiveData<Boolean> = MutableLiveData(true)
|
||||
|
||||
override fun updateLoginField(login: String) = Unit
|
||||
override fun updatePasswordField(password: String) = Unit
|
||||
override fun updateRememberCredential(rememberCredential: Boolean) = Unit
|
||||
override fun clearState() = Unit
|
||||
|
||||
override fun login() = Unit
|
||||
|
||||
@Composable
|
||||
override fun PrepareLoginWithGoogle() = Unit
|
||||
override fun loginWithGoogle() = Unit
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.pixelized.biblib.ui.viewmodel.authentication
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
|
||||
interface IAuthenticationViewModel {
|
||||
val state: LiveData<State>
|
||||
|
||||
fun clearState()
|
||||
|
||||
fun autoLogin()
|
||||
|
||||
fun login(login: String, password: String, rememberCredential: Boolean)
|
||||
|
||||
@Composable
|
||||
fun PrepareLoginWithGoogle()
|
||||
fun loginWithGoogle()
|
||||
|
||||
sealed class State {
|
||||
object Initial : State()
|
||||
object Loading : State()
|
||||
data class Finished(val logged: Boolean) : State()
|
||||
data class Error(val exception: Exception) : State()
|
||||
}
|
||||
|
||||
class Mock(state: State = State.Initial) : IAuthenticationViewModel {
|
||||
override val state: LiveData<State> = MutableLiveData(state)
|
||||
|
||||
override fun clearState() = Unit
|
||||
override fun autoLogin() = Unit
|
||||
override fun login(login: String, password: String, rememberCredential: Boolean) = Unit
|
||||
|
||||
@Composable
|
||||
override fun PrepareLoginWithGoogle() = Unit
|
||||
override fun loginWithGoogle() = Unit
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package com.pixelized.biblib.ui.viewmodel.book
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import com.pixelized.biblib.model.Book
|
||||
import com.pixelized.biblib.network.client.IBibLibClient
|
||||
import com.pixelized.biblib.network.factory.BookFactory
|
||||
import com.pixelized.biblib.repository.apiCache.IAPICacheRepository
|
||||
import com.pixelized.biblib.repository.book.IBookRepository
|
||||
import com.pixelized.biblib.ui.data.BookThumbnailUio
|
||||
import com.pixelized.biblib.utils.injection.inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class BooksViewModel : ViewModel(), IBooksViewModel {
|
||||
private val bookRepository: IBookRepository by inject()
|
||||
private val client: IBibLibClient by inject()
|
||||
private val apiCache: IAPICacheRepository by inject()
|
||||
|
||||
private val _state = MutableLiveData<IBooksViewModel.State>()
|
||||
override val state: LiveData<IBooksViewModel.State> get() = _state
|
||||
|
||||
private val source
|
||||
get() = bookRepository.getBook()
|
||||
.map { it.toThumbnail() }
|
||||
.asPagingSourceFactory(Dispatchers.Default)
|
||||
|
||||
override val books: Flow<PagingData<BookThumbnailUio>> = Pager(
|
||||
config = PagingConfig(pageSize = 20),
|
||||
pagingSourceFactory = source
|
||||
).flow
|
||||
|
||||
override fun updateBooks() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
_state.postValue(IBooksViewModel.State.Initial)
|
||||
_state.postValue(IBooksViewModel.State.Loading)
|
||||
try {
|
||||
val updated = loadNewBooks() && loadAllBooks()
|
||||
_state.postValue(IBooksViewModel.State.Finished(updated))
|
||||
} catch (exception: Exception) {
|
||||
Log.e("BooksViewModel", exception.message, exception)
|
||||
_state.postValue(IBooksViewModel.State.Error(exception))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadNewBooks(): Boolean {
|
||||
val cached = apiCache.new
|
||||
val updated = client.service.new()
|
||||
return if (cached != updated) {
|
||||
apiCache.new = updated
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadAllBooks(): Boolean {
|
||||
client.service.list().let { response ->
|
||||
val factory = BookFactory()
|
||||
val books = response.data?.map { dto -> factory.fromListResponseToBook(dto, false) }
|
||||
books?.let { data -> bookRepository.update(data) }
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun Book.toThumbnail() = BookThumbnailUio(
|
||||
id = id,
|
||||
genre = genre?.joinToString { it.name } ?: "",
|
||||
title = title,
|
||||
author = author.joinToString { it.name },
|
||||
date = releaseDate.toString(),
|
||||
series = series?.name,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package com.pixelized.biblib.ui.viewmodel.book
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.PagingData
|
||||
import com.pixelized.biblib.ui.data.BookThumbnailUio
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
interface IBooksViewModel {
|
||||
val state: LiveData<State>
|
||||
|
||||
val books: Flow<PagingData<BookThumbnailUio>>
|
||||
|
||||
fun updateBooks()
|
||||
|
||||
sealed class State {
|
||||
object Initial : State()
|
||||
object Loading : State()
|
||||
data class Finished(val updated: Boolean) : State()
|
||||
data class Error(val exception: Exception) : State()
|
||||
}
|
||||
|
||||
class Mock(initial: State = State.Initial) : IBooksViewModel {
|
||||
override val state: LiveData<State> = MutableLiveData(initial)
|
||||
|
||||
override val books: Flow<PagingData<BookThumbnailUio>>
|
||||
get() = flowOf()
|
||||
|
||||
override fun updateBooks() = Unit
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.pixelized.biblib.ui.viewmodel.credential
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.pixelized.biblib.repository.credential.ICredentialRepository
|
||||
import com.pixelized.biblib.utils.injection.inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class CredentialViewModel : ViewModel(), ICredentialViewModel {
|
||||
private val credentialRepository: ICredentialRepository by inject()
|
||||
|
||||
private val _login = MutableLiveData<String>()
|
||||
override val login: LiveData<String?> get() = _login
|
||||
|
||||
private val _password = MutableLiveData<String>()
|
||||
override val password: LiveData<String?> get() = _password
|
||||
|
||||
private val _rememberCredential = MutableLiveData<Boolean>()
|
||||
override val rememberCredential: LiveData<Boolean> get() = _rememberCredential
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
_login.value = credentialRepository.login
|
||||
_password.value = credentialRepository.password
|
||||
_rememberCredential.value = credentialRepository.rememberCredential
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateLoginField(login: String) {
|
||||
_login.postValue(login)
|
||||
}
|
||||
|
||||
override fun updatePasswordField(password: String) {
|
||||
_password.postValue(password)
|
||||
}
|
||||
|
||||
override fun updateRememberCredential(rememberCredential: Boolean) {
|
||||
viewModelScope.launch {
|
||||
_rememberCredential.postValue(rememberCredential)
|
||||
credentialRepository.rememberCredential = rememberCredential
|
||||
if (rememberCredential) {
|
||||
credentialRepository.login = login.value
|
||||
credentialRepository.password = password.value
|
||||
} else {
|
||||
credentialRepository.login = null
|
||||
credentialRepository.password = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.pixelized.biblib.ui.viewmodel.credential
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
|
||||
interface ICredentialViewModel {
|
||||
|
||||
val login: LiveData<String?>
|
||||
val password: LiveData<String?>
|
||||
val rememberCredential: LiveData<Boolean>
|
||||
|
||||
fun updateLoginField(login: String)
|
||||
fun updatePasswordField(password: String)
|
||||
fun updateRememberCredential(rememberCredential: Boolean)
|
||||
|
||||
// Todo
|
||||
// fun validateForm()
|
||||
|
||||
class Mock : ICredentialViewModel {
|
||||
override val login: LiveData<String?> = MutableLiveData("")
|
||||
override val password: LiveData<String?> = MutableLiveData("")
|
||||
override val rememberCredential: LiveData<Boolean> = MutableLiveData(true)
|
||||
|
||||
override fun updateLoginField(login: String) = Unit
|
||||
override fun updatePasswordField(password: String) = Unit
|
||||
override fun updateRememberCredential(rememberCredential: Boolean) = Unit
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
package com.pixelized.biblib.ui.viewmodel.initialisation
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
|
||||
interface IInitialisation {
|
||||
|
||||
val state: LiveData<State>
|
||||
|
||||
fun loadApplication()
|
||||
|
||||
fun loadBook()
|
||||
|
||||
@Stable
|
||||
sealed class State {
|
||||
@Stable
|
||||
object Initial : State()
|
||||
|
||||
@Stable
|
||||
object Loading : State()
|
||||
|
||||
@Stable
|
||||
class Finished(val needLogin: Boolean) : State()
|
||||
|
||||
@Stable
|
||||
class Error(val exception: Exception) : State()
|
||||
}
|
||||
|
||||
class Mock(private val value: State = State.Initial) : IInitialisation {
|
||||
override val state get() = MutableLiveData(value)
|
||||
override fun loadApplication() = Unit
|
||||
override fun loadBook() = Unit
|
||||
}
|
||||
}
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
package com.pixelized.biblib.ui.viewmodel.initialisation
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.pixelized.biblib.network.client.IBibLibClient
|
||||
import com.pixelized.biblib.network.data.query.AuthLoginQuery
|
||||
import com.pixelized.biblib.network.factory.BookFactory
|
||||
import com.pixelized.biblib.repository.apiCache.IAPICacheRepository
|
||||
import com.pixelized.biblib.repository.book.IBookRepository
|
||||
import com.pixelized.biblib.repository.credential.ICredentialRepository
|
||||
import com.pixelized.biblib.repository.googleSignIn.IGoogleSingInRepository
|
||||
import com.pixelized.biblib.ui.viewmodel.initialisation.IInitialisation.State.*
|
||||
import com.pixelized.biblib.utils.injection.inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
class InitialisationViewModel : ViewModel(), IInitialisation {
|
||||
private val credentialRepository: ICredentialRepository by inject()
|
||||
private val bookRepository: IBookRepository by inject()
|
||||
private val googleSignIn: IGoogleSingInRepository by inject()
|
||||
private val client: IBibLibClient by inject()
|
||||
private val apiCache: IAPICacheRepository by inject()
|
||||
|
||||
private val _state = MutableLiveData<IInitialisation.State>()
|
||||
override val state: LiveData<IInitialisation.State> get() = _state
|
||||
|
||||
override fun loadApplication() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
_state.postValue(Initial)
|
||||
_state.postValue(Loading)
|
||||
delay(2000)
|
||||
try {
|
||||
val loggedIn = loginWithGoogle() || loginWithCredential()
|
||||
if (loggedIn) {
|
||||
loadNewBooks() && loadAllBooks()
|
||||
}
|
||||
_state.postValue(Finished(needLogin = loggedIn.not()))
|
||||
} catch (exception: Exception) {
|
||||
_state.postValue(Error(exception))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadBook() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
_state.postValue(Initial)
|
||||
_state.postValue(Loading)
|
||||
try {
|
||||
loadNewBooks() && loadAllBooks()
|
||||
_state.postValue(Finished(needLogin = false))
|
||||
} catch (exception: Exception) {
|
||||
_state.postValue(Error(exception))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loginWithGoogle(): Boolean {
|
||||
val googleToken = googleSignIn.lastGoogleToken
|
||||
return if (googleToken != null) {
|
||||
try {
|
||||
client.service.loginWithGoogle(googleToken).let { response ->
|
||||
if (response.token != null) {
|
||||
client.updateBearerToken(response.token)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loginWithCredential(): Boolean {
|
||||
val login = credentialRepository.login
|
||||
val password = credentialRepository.password
|
||||
return if (login != null && password != null) {
|
||||
try {
|
||||
val query = AuthLoginQuery(login, password)
|
||||
client.service.login(query).let { response ->
|
||||
if (response.token != null) {
|
||||
client.updateBearerToken(response.token)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadNewBooks(): Boolean {
|
||||
val cached = apiCache.new
|
||||
val updated = client.service.new()
|
||||
return if (cached != updated) {
|
||||
apiCache.new = updated
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadAllBooks(): Boolean {
|
||||
client.service.list().let { response ->
|
||||
val factory = BookFactory()
|
||||
val books = response.data?.map { dto -> factory.fromListResponseToBook(dto, false) }
|
||||
books?.let { data -> bookRepository.update(data) }
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import androidx.lifecycle.LiveData
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import com.pixelized.biblib.ui.data.BookUio
|
||||
|
||||
interface INavigation {
|
||||
interface INavigationViewModel {
|
||||
val screen: LiveData<Screen>
|
||||
val page: LiveData<Page>
|
||||
|
||||
|
|
@ -24,7 +24,7 @@ interface INavigation {
|
|||
data class Detail(val book: BookUio) : Page()
|
||||
}
|
||||
|
||||
class Mock(screen: Screen = Screen.SplashScreen, page: Page = Page.HomePage) : INavigation {
|
||||
class Mock(screen: Screen = Screen.SplashScreen, page: Page = Page.HomePage) : INavigationViewModel {
|
||||
override val screen: LiveData<Screen> = MutableLiveData(screen)
|
||||
override val page: LiveData<Page> = MutableLiveData(page)
|
||||
|
||||
|
|
@ -3,12 +3,11 @@ package com.pixelized.biblib.ui.viewmodel.navigation
|
|||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation
|
||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation.Page
|
||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigation.Screen
|
||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel.Page
|
||||
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel.Screen
|
||||
import java.util.*
|
||||
|
||||
class NavigationViewModel : ViewModel(), INavigation {
|
||||
class NavigationViewModel : ViewModel(), INavigationViewModel {
|
||||
|
||||
private val stack = Stack<Page>()
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,12 @@
|
|||
<string name="action_google_sign_in">Sign in with Google</string>
|
||||
|
||||
<string name="error_generic">Oops!</string>
|
||||
<string name="error_authentication">Oops, connection failed!</string>
|
||||
<string name="error_book">Oops! library download failed!</string>
|
||||
|
||||
<string name="loading_authentication">Entering the Imperial Library of Trantor.</string>
|
||||
<string name="loading_book">Entering the Imperial Library of Trantor.</string>
|
||||
<string name="loading_book">Downloading the Imperial Library of Trantor.</string>
|
||||
|
||||
<string name="success_authentication">Authentication successful</string>
|
||||
<string name="success_book">Library successfully loaded</string>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue