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
				
			
		| 
						 | 
				
			
			@ -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