From 1e58752008fe78a886962304c41ec36e2b0efdc3 Mon Sep 17 00:00:00 2001 From: Thomas Andres Gomez Date: Tue, 11 May 2021 23:34:22 +0200 Subject: [PATCH] Refactor application loading + Paging on main Page --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 3 +- .../pixelized/biblib/database/dao/BookDao.kt | 5 + .../biblib/database/relation/BookRelation.kt | 8 +- .../biblib/network/client/BibLibClient.kt | 10 +- .../biblib/network/client/IBibLibClient.kt | 2 +- .../biblib/repository/book/BookRepository.kt | 51 +++++- .../biblib/repository/book/IBookRepository.kt | 8 +- .../com/pixelized/biblib/ui/MainActivity.kt | 2 +- .../items/dialog/CrossFadeOverlay.kt | 2 +- .../ui/composable/pages/HomePageComposable.kt | 35 ++-- .../screen/LoginScreenComposable.kt | 154 ++++++++++-------- .../composable/screen/MainScreenComposable.kt | 40 ++--- .../screen/SplashScreenComposable.kt | 114 +++++++++---- .../authentication/AuthenticationViewModel.kt | 127 ++++++++------- .../authentication/IAuthentication.kt | 48 ------ .../IAuthenticationViewModel.kt | 38 +++++ .../ui/viewmodel/book/BooksViewModel.kt | 82 ++++++++++ .../ui/viewmodel/book/IBooksViewModel.kt | 32 ++++ .../credential/CredentialViewModel.kt | 54 ++++++ .../credential/ICredentialViewModel.kt | 28 ++++ .../initialisation/IInitialisation.kt | 35 ---- .../initialisation/InitialisationViewModel.kt | 122 -------------- ...INavigation.kt => INavigationViewModel.kt} | 4 +- .../navigation/NavigationViewModel.kt | 7 +- app/src/main/res/values/strings.xml | 6 +- 26 files changed, 596 insertions(+), 423 deletions(-) delete mode 100644 app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/IAuthentication.kt create mode 100644 app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/IAuthenticationViewModel.kt create mode 100644 app/src/main/java/com/pixelized/biblib/ui/viewmodel/book/BooksViewModel.kt create mode 100644 app/src/main/java/com/pixelized/biblib/ui/viewmodel/book/IBooksViewModel.kt create mode 100644 app/src/main/java/com/pixelized/biblib/ui/viewmodel/credential/CredentialViewModel.kt create mode 100644 app/src/main/java/com/pixelized/biblib/ui/viewmodel/credential/ICredentialViewModel.kt delete mode 100644 app/src/main/java/com/pixelized/biblib/ui/viewmodel/initialisation/IInitialisation.kt delete mode 100644 app/src/main/java/com/pixelized/biblib/ui/viewmodel/initialisation/InitialisationViewModel.kt rename app/src/main/java/com/pixelized/biblib/ui/viewmodel/navigation/{INavigation.kt => INavigationViewModel.kt} (93%) diff --git a/app/build.gradle b/app/build.gradle index 3f4a8b6..9494dbb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a2bb010..3b01cb1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,8 +7,7 @@ + @Transaction + @Query("SELECT * FROM ${BookDbo.TABLE}") + fun getBook(): DataSource.Factory + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(vararg books: BookDbo) diff --git a/app/src/main/java/com/pixelized/biblib/database/relation/BookRelation.kt b/app/src/main/java/com/pixelized/biblib/database/relation/BookRelation.kt index 0a50a66..1d10ecc 100644 --- a/app/src/main/java/com/pixelized/biblib/database/relation/BookRelation.kt +++ b/app/src/main/java/com/pixelized/biblib/database/relation/BookRelation.kt @@ -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, + val genres: List?, @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?, ) \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/network/client/BibLibClient.kt b/app/src/main/java/com/pixelized/biblib/network/client/BibLibClient.kt index 2add48a..6d295a3 100644 --- a/app/src/main/java/com/pixelized/biblib/network/client/BibLibClient.kt +++ b/app/src/main/java/com/pixelized/biblib/network/client/BibLibClient.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/network/client/IBibLibClient.kt b/app/src/main/java/com/pixelized/biblib/network/client/IBibLibClient.kt index d5028cb..7216ffb 100644 --- a/app/src/main/java/com/pixelized/biblib/network/client/IBibLibClient.kt +++ b/app/src/main/java/com/pixelized/biblib/network/client/IBibLibClient.kt @@ -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" diff --git a/app/src/main/java/com/pixelized/biblib/repository/book/BookRepository.kt b/app/src/main/java/com/pixelized/biblib/repository/book/BookRepository.kt index bb5f719..bfd7f30 100644 --- a/app/src/main/java/com/pixelized/biblib/repository/book/BookRepository.kt +++ b/app/src/main/java/com/pixelized/biblib/repository/book/BookRepository.kt @@ -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) { + override fun getAll(): List = + database.bookDao().getAll().map { it.toBook() } + + override fun getBook(): DataSource.Factory = + database.bookDao().getBook().map { it.toBook() } + + override suspend fun update(data: List) { + Log.d("pouet", "BookRepository#update(): $data") val authors = mutableSetOf() val genres = mutableSetOf() val series = mutableSetOf() @@ -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, + ) } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/repository/book/IBookRepository.kt b/app/src/main/java/com/pixelized/biblib/repository/book/IBookRepository.kt index 958c819..8705890 100644 --- a/app/src/main/java/com/pixelized/biblib/repository/book/IBookRepository.kt +++ b/app/src/main/java/com/pixelized/biblib/repository/book/IBookRepository.kt @@ -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) + + fun getAll(): List + + fun getBook(): DataSource.Factory + + suspend fun update(data: List) } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/MainActivity.kt b/app/src/main/java/com/pixelized/biblib/ui/MainActivity.kt index 8064e68..2db0d35 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/MainActivity.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/MainActivity.kt @@ -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() { diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/items/dialog/CrossFadeOverlay.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/items/dialog/CrossFadeOverlay.kt index 914a2b1..e5e36b1 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/composable/items/dialog/CrossFadeOverlay.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/composable/items/dialog/CrossFadeOverlay.kt @@ -30,7 +30,7 @@ fun CrossFadeOverlay( modifier = Modifier .fillMaxWidth() .fillMaxHeight() - .alpha(.25f) + .alpha(.75f) .background(Color.Black) ) // Overlay content. diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/pages/HomePageComposable.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/pages/HomePageComposable.kt index ed8cb28..399ac62 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/composable/pages/HomePageComposable.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/composable/pages/HomePageComposable.kt @@ -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 = 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)) } } } diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/screen/LoginScreenComposable.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/screen/LoginScreenComposable.kt index b4770bb..0f2146b 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/composable/screen/LoginScreenComposable.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/composable/screen/LoginScreenComposable.kt @@ -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(), - authentication: IAuthentication = viewModel(), - initialisation: IInitialisation = viewModel() + navigationViewModel: INavigationViewModel = viewModel(), + credentialViewModel: ICredentialViewModel = viewModel(), + authenticationViewModel: IAuthenticationViewModel = viewModel(), + bookViewModel: IBooksViewModel = viewModel() ) { 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 = authentication.login.observeAsState() + val login: State = 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, + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/screen/MainScreenComposable.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/screen/MainScreenComposable.kt index 430402b..6f7f616 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/composable/screen/MainScreenComposable.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/composable/screen/MainScreenComposable.kt @@ -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: INavigationViewModel = viewModel(), + booksViewModel: IBooksViewModel = viewModel(), ) { - 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, diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/screen/SplashScreenComposable.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/screen/SplashScreenComposable.kt index 43e1677..320d067 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/composable/screen/SplashScreenComposable.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/composable/screen/SplashScreenComposable.kt @@ -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(), - navigation: INavigation = viewModel(), + authenticationViewModel: IAuthenticationViewModel = viewModel(), + bookViewModel: IBooksViewModel = viewModel(), + navigationViewModel: INavigationViewModel = viewModel(), 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 + ) } } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/AuthenticationViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/AuthenticationViewModel.kt index 6ba1927..e511062 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/AuthenticationViewModel.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/AuthenticationViewModel.kt @@ -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? = null private val _state = MutableLiveData(State.Initial) override val state: LiveData get() = _state - private val _login = MutableLiveData() - override val login: LiveData get() = _login - - private val _password = MutableLiveData() - override val password: LiveData get() = _password - - private val _rememberCredential = MutableLiveData() - override val rememberCredential: LiveData 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 + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/IAuthentication.kt b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/IAuthentication.kt deleted file mode 100644 index 57e39fa..0000000 --- a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/IAuthentication.kt +++ /dev/null @@ -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 - val login: LiveData - val password: LiveData - val rememberCredential: LiveData - - 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 = MutableLiveData(state) - override val login: LiveData = MutableLiveData("") - override val password: LiveData = MutableLiveData("") - override val rememberCredential: LiveData = 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 - } -} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/IAuthenticationViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/IAuthenticationViewModel.kt new file mode 100644 index 0000000..99adf91 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/IAuthenticationViewModel.kt @@ -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 + + 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 = 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/book/BooksViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/book/BooksViewModel.kt new file mode 100644 index 0000000..dc3e378 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/book/BooksViewModel.kt @@ -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() + override val state: LiveData get() = _state + + private val source + get() = bookRepository.getBook() + .map { it.toThumbnail() } + .asPagingSourceFactory(Dispatchers.Default) + + override val books: Flow> = 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, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/book/IBooksViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/book/IBooksViewModel.kt new file mode 100644 index 0000000..7608285 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/book/IBooksViewModel.kt @@ -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 + + val books: Flow> + + 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 = MutableLiveData(initial) + + override val books: Flow> + get() = flowOf() + + override fun updateBooks() = Unit + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/credential/CredentialViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/credential/CredentialViewModel.kt new file mode 100644 index 0000000..e3410f9 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/credential/CredentialViewModel.kt @@ -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() + override val login: LiveData get() = _login + + private val _password = MutableLiveData() + override val password: LiveData get() = _password + + private val _rememberCredential = MutableLiveData() + override val rememberCredential: LiveData 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 + } + } + } +} + diff --git a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/credential/ICredentialViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/credential/ICredentialViewModel.kt new file mode 100644 index 0000000..ae80ff5 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/credential/ICredentialViewModel.kt @@ -0,0 +1,28 @@ +package com.pixelized.biblib.ui.viewmodel.credential + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData + +interface ICredentialViewModel { + + val login: LiveData + val password: LiveData + val rememberCredential: LiveData + + fun updateLoginField(login: String) + fun updatePasswordField(password: String) + fun updateRememberCredential(rememberCredential: Boolean) + + // Todo + // fun validateForm() + + class Mock : ICredentialViewModel { + override val login: LiveData = MutableLiveData("") + override val password: LiveData = MutableLiveData("") + override val rememberCredential: LiveData = MutableLiveData(true) + + override fun updateLoginField(login: String) = Unit + override fun updatePasswordField(password: String) = Unit + override fun updateRememberCredential(rememberCredential: Boolean) = Unit + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/initialisation/IInitialisation.kt b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/initialisation/IInitialisation.kt deleted file mode 100644 index 4d9d250..0000000 --- a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/initialisation/IInitialisation.kt +++ /dev/null @@ -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 - - 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 - } -} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/initialisation/InitialisationViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/initialisation/InitialisationViewModel.kt deleted file mode 100644 index f3534a7..0000000 --- a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/initialisation/InitialisationViewModel.kt +++ /dev/null @@ -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() - override val state: LiveData 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 - } -} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/navigation/INavigation.kt b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/navigation/INavigationViewModel.kt similarity index 93% rename from app/src/main/java/com/pixelized/biblib/ui/viewmodel/navigation/INavigation.kt rename to app/src/main/java/com/pixelized/biblib/ui/viewmodel/navigation/INavigationViewModel.kt index 0a4aa9c..89c3fbf 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/navigation/INavigation.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/navigation/INavigationViewModel.kt @@ -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 val page: LiveData @@ -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 = MutableLiveData(screen) override val page: LiveData = MutableLiveData(page) diff --git a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/navigation/NavigationViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/navigation/NavigationViewModel.kt index c630c93..0c700df 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/navigation/NavigationViewModel.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/navigation/NavigationViewModel.kt @@ -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() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4f77ddd..c748622 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -10,8 +10,12 @@ Sign in with Google Oops! + Oops, connection failed! + Oops! library download failed! + Entering the Imperial Library of Trantor. - Entering the Imperial Library of Trantor. + Downloading the Imperial Library of Trantor. + Authentication successful Library successfully loaded