diff --git a/app/src/main/java/com/pixelized/biblib/repository/apiCache/APICacheRepository.kt b/app/src/main/java/com/pixelized/biblib/repository/apiCache/APICacheRepository.kt index baaf092..2a408c6 100644 --- a/app/src/main/java/com/pixelized/biblib/repository/apiCache/APICacheRepository.kt +++ b/app/src/main/java/com/pixelized/biblib/repository/apiCache/APICacheRepository.kt @@ -14,20 +14,11 @@ class APICacheRepository : IAPICacheRepository { get() = preferences.new?.let { gson.fromJson(it, BookListResponse::class.java) } set(value) = gson.toJson(value).let { preferences.new = it } - override var list: BookListResponse? - get() = preferences.list?.let { gson.fromJson(it, BookListResponse::class.java) } - set(value) = gson.toJson(value).let { preferences.list = it } - private var SharedPreferences.new: String? get() = getString(NEW, null) set(value) = edit { putString(NEW, value) } - private var SharedPreferences.list: String? - get() = getString(LIST, null) - set(value) = edit { putString(LIST, value) } - companion object { const val NEW = "NEW" - const val LIST = "LIST" } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/repository/apiCache/IAPICacheRepository.kt b/app/src/main/java/com/pixelized/biblib/repository/apiCache/IAPICacheRepository.kt index 0e9b365..69e20ed 100644 --- a/app/src/main/java/com/pixelized/biblib/repository/apiCache/IAPICacheRepository.kt +++ b/app/src/main/java/com/pixelized/biblib/repository/apiCache/IAPICacheRepository.kt @@ -4,5 +4,4 @@ import com.pixelized.biblib.network.data.response.BookListResponse interface IAPICacheRepository { var new: BookListResponse? - var list: BookListResponse? } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/items/dialog/LoadingCard.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/items/dialog/LoadingCard.kt index 5459465..0573c57 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/composable/items/dialog/LoadingCard.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/composable/items/dialog/LoadingCard.kt @@ -52,7 +52,7 @@ fun LoadingCard( private fun LoadingCardLightPreview() { BibLibTheme(darkTheme = false) { LoadingCard( - message = stringResource(id = R.string.loading) + message = stringResource(id = R.string.loading_authentication) ) } } @@ -62,7 +62,7 @@ private fun LoadingCardLightPreview() { private fun LoadingCardDarkPreview() { BibLibTheme(darkTheme = true) { LoadingCard( - message = stringResource(id = R.string.loading) + message = stringResource(id = R.string.loading_book) ) } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/items/dialog/SuccesCard.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/items/dialog/SuccesCard.kt index e074f41..4990b52 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/composable/items/dialog/SuccesCard.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/composable/items/dialog/SuccesCard.kt @@ -60,7 +60,7 @@ fun SuccessCard( @Composable private fun SuccessLightPreview() { BibLibTheme(darkTheme = false) { - SuccessCard(message = stringResource(id = R.string.authentication_success)) + SuccessCard(message = stringResource(id = R.string.success_authentication)) } } @@ -68,6 +68,6 @@ private fun SuccessLightPreview() { @Composable private fun SuccessDarkPreview() { BibLibTheme(darkTheme = true) { - SuccessCard(message = stringResource(id = R.string.authentication_success)) + SuccessCard(message = stringResource(id = R.string.success_authentication)) } } \ No newline at end of file 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 61c5790..b4770bb 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 @@ -45,15 +45,19 @@ 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.navigation.NavigationViewModel -import kotlinx.coroutines.delay +private const val LE_LOAD_BOOK = "LE_LOAD_BOOK" + @Composable fun LoginScreenComposable( navigation: INavigation = viewModel(), authentication: IAuthentication = viewModel(), + initialisation: IInitialisation = viewModel() ) { Box( modifier = Modifier @@ -61,10 +65,10 @@ fun LoginScreenComposable( .fillMaxHeight() ) { authentication.PrepareLoginWithGoogle() - LoginScreenNavigationComposable(navigation, authentication) + LoginScreenNavigationComposable(navigation, authentication, initialisation) LoginScreenContentComposable(authentication) - LoginScreenDialogComposable(authentication) + LoginScreenDialogComposable(authentication, initialisation) } } @@ -91,49 +95,87 @@ private fun LoginScreenContentComposable( @OptIn(ExperimentalAnimationApi::class) @Composable private fun LoginScreenDialogComposable( - authentication: IAuthentication + authentication: IAuthentication, + initialisation: IInitialisation, ) { - val state = authentication.state.observeAsState() + val authenticationState = authentication.state.observeAsState() + val bookLoadingState = initialisation.state.observeAsState() + CrossFadeOverlay( modifier = Modifier.clickable { - if (state.value is IAuthentication.State.Error) { + if (authenticationState.value is IAuthentication.State.Error) { authentication.clearState() } }, - visible = (state.value is IAuthentication.State.Initial).not() + visible = (authenticationState.value is IAuthentication.State.Initial + && bookLoadingState.value is IInitialisation.State.Initial).not() ) { AnimatedVisibility( modifier = Modifier.align(Alignment.Center), - visible = state.value is IAuthentication.State.Error, + visible = authenticationState.value is IAuthentication.State.Error, initiallyVisible = false, enter = expandVertically(Alignment.CenterVertically), exit = shrinkVertically(Alignment.CenterVertically), ) { ErrorCard( message = stringResource(id = R.string.error_generic), - exception = (state.value as? IAuthentication.State.Error)?.exception + exception = (authenticationState.value as? IAuthentication.State.Error)?.exception ) } AnimatedVisibility( modifier = Modifier.align(Alignment.Center), - visible = state.value is IAuthentication.State.Loading, + visible = authenticationState.value is IAuthentication.State.Loading, initiallyVisible = false, enter = expandVertically(Alignment.CenterVertically), exit = shrinkVertically(Alignment.CenterVertically), ) { LoadingCard( - message = stringResource(id = R.string.loading) + message = stringResource(id = R.string.loading_authentication) ) } AnimatedVisibility( modifier = Modifier.align(Alignment.Center), - visible = state.value is IAuthentication.State.Connect, + visible = authenticationState.value is IAuthentication.State.Connect, initiallyVisible = false, enter = expandVertically(Alignment.CenterVertically), exit = shrinkVertically(Alignment.CenterVertically), ) { SuccessCard( - message = stringResource(id = R.string.authentication_success) + message = stringResource(id = R.string.success_authentication) + ) + } + AnimatedVisibility( + modifier = Modifier.align(Alignment.Center), + visible = bookLoadingState.value is IInitialisation.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 + ) + } + AnimatedVisibility( + modifier = Modifier.align(Alignment.Center), + visible = bookLoadingState.value is IInitialisation.State.Loading, + initiallyVisible = false, + enter = expandVertically(Alignment.CenterVertically), + exit = shrinkVertically(Alignment.CenterVertically), + ) { + LoadingCard( + message = stringResource(id = R.string.loading_book) + ) + } + AnimatedVisibility( + modifier = Modifier.align(Alignment.Center), + visible = bookLoadingState.value is IInitialisation.State.Finished, + initiallyVisible = false, + enter = expandVertically(Alignment.CenterVertically), + exit = shrinkVertically(Alignment.CenterVertically), + ) { + SuccessCard( + message = stringResource(id = R.string.success_book) ) } } @@ -142,15 +184,21 @@ private fun LoginScreenDialogComposable( @Composable private fun LoginScreenNavigationComposable( navigation: INavigation, - authentication: IAuthentication + authentication: IAuthentication, + initialisation: IInitialisation, ) { - val state = authentication.state.observeAsState() - if (state.value == IAuthentication.State.Connect) { - LaunchedEffect(key1 = "navigateTo(MainScreen)") { - delay(1000) - navigation.navigateTo(INavigation.Screen.MainScreen) + val authenticationState = authentication.state.observeAsState() + val bookLoadingState = initialisation.state.observeAsState() + + if (authenticationState.value == IAuthentication.State.Connect) { + LaunchedEffect(LE_LOAD_BOOK) { + initialisation.loadBook() } } + + if (bookLoadingState.value is IInitialisation.State.Finished) { + navigation.navigateTo(INavigation.Screen.MainScreen) + } } @Composable @@ -317,6 +365,7 @@ fun LoginScreenComposablePreview() { BibLibTheme { val navigationViewModel = INavigation.Mock() val authenticationViewModel = IAuthentication.Mock() - LoginScreenComposable(navigationViewModel, authenticationViewModel) + val initialisation = IInitialisation.Mock() + LoginScreenComposable(navigationViewModel, authenticationViewModel, initialisation) } } \ No newline at end of file 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 33b3f0a..43e1677 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 @@ -2,11 +2,14 @@ package com.pixelized.biblib.ui.composable.screen import androidx.compose.animation.* import androidx.compose.animation.core.tween +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.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -15,6 +18,8 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.pixelized.biblib.BuildConfig 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 @@ -23,12 +28,16 @@ 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" @Preview @Composable fun SplashScreenComposablePreview() { BibLibTheme { - SplashScreenComposable(IInitialisation.Mock(), INavigation.Mock()) + val initialisation = IInitialisation.Mock(IInitialisation.State.Loading) + val navigation = INavigation.Mock() + SplashScreenComposable(initialisation, navigation, true) } } @@ -36,92 +45,138 @@ fun SplashScreenComposablePreview() { @Composable fun SplashScreenComposable( initialisation: IInitialisation = viewModel(), - navigation: INavigation = viewModel() + navigation: INavigation = viewModel(), + initiallyVisible: Boolean = false, +) { + val state by initialisation.state.observeAsState() + + LaunchedEffect(LAUNCH_EFFECT_LOAD_APPLICATION) { + initialisation.loadApplication() + } + + ContentComposable(state = state, initiallyVisible = initiallyVisible) + DialogComposable(state = state) + NavigationComposable(navigation = navigation, state = state) +} + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun ContentComposable( + state: IInitialisation.State?, + duration: Int = ANIMATION_DURATION, + initiallyVisible: Boolean = false, ) { - val duration = 1000 val typography = MaterialTheme.typography - initialisation.LoadApplication { - val visible = it !is IInitialisation.State.Finished + val visible = state !is IInitialisation.State.Finished - Box( + Box( + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth() + .padding(16.dp), + ) { + Column( modifier = Modifier - .fillMaxHeight() - .fillMaxWidth() - .padding(16.dp), + .width(240.dp) + .align(Alignment.Center) ) { - Column( - modifier = Modifier - .width(240.dp) - .align(Alignment.Center) - ) { - AnimatedVisibility( - visible = visible, - initiallyVisible = false, - enter = fadeIn(animationSpec = tween(duration)) - + slideInVertically( - initialOffsetY = { height -> -height }, - animationSpec = tween(duration) - ), - exit = fadeOut(animationSpec = tween(duration)) - + slideOutVertically( - targetOffsetY = { height -> -height }, - animationSpec = tween(duration) - ), - ) { - Text( - style = typography.h4, - text = "Welcome to" - ) - } - AnimatedVisibility( - modifier = Modifier.align(Alignment.End), - visible = visible, - initiallyVisible = false, - enter = fadeIn(animationSpec = tween(duration)) - + slideInVertically( - initialOffsetY = { height -> height }, - animationSpec = tween(duration) - ), - exit = fadeOut(animationSpec = tween(duration)) - + slideOutVertically( - targetOffsetY = { height -> height }, - animationSpec = tween(duration) - ), - ) { - Text( - style = typography.h4, - text = stringResource(id = R.string.app_name) - ) - } - } - AnimatedVisibility( - modifier = Modifier.align(Alignment.BottomEnd), visible = visible, - enter = fadeIn(animationSpec = tween(duration)), - exit = fadeOut(animationSpec = tween(duration)), + initiallyVisible = initiallyVisible, + enter = fadeIn(animationSpec = tween(duration)) + + slideInVertically( + initialOffsetY = { height -> -height }, + animationSpec = tween(duration) + ), + exit = fadeOut(animationSpec = tween(duration)) + + slideOutVertically( + targetOffsetY = { height -> -height }, + animationSpec = tween(duration) + ), ) { Text( - style = typography.caption, - text = stringResource( - R.string.app_version, - BuildConfig.BUILD_TYPE.toUpperCase(Locale.getDefault()), - BuildConfig.VERSION_NAME, - BuildConfig.VERSION_CODE - ) + style = typography.h4, + text = "Welcome to" + ) + } + AnimatedVisibility( + modifier = Modifier.align(Alignment.End), + visible = visible, + initiallyVisible = initiallyVisible, + enter = fadeIn(animationSpec = tween(duration)) + + slideInVertically( + initialOffsetY = { height -> height }, + animationSpec = tween(duration) + ), + exit = fadeOut(animationSpec = tween(duration)) + + slideOutVertically( + targetOffsetY = { height -> height }, + animationSpec = tween(duration) + ), + ) { + Text( + style = typography.h4, + text = stringResource(id = R.string.app_name) ) } } - if (it is IInitialisation.State.Finished) { - LaunchedEffect(key1 = "SplashScreen.navigateTo()") { - delay(1000) - if (it.needLogin) { - navigation.navigateTo(INavigation.Screen.LoginScreen) - } else { - navigation.navigateTo(INavigation.Screen.MainScreen) - } - } + AnimatedVisibility( + modifier = Modifier.align(Alignment.BottomEnd), + visible = visible, + initiallyVisible = initiallyVisible, + enter = fadeIn(animationSpec = tween(duration)), + exit = fadeOut(animationSpec = tween(duration)), + ) { + Text( + style = typography.caption, + text = stringResource( + R.string.app_version, + BuildConfig.BUILD_TYPE.toUpperCase(Locale.getDefault()), + BuildConfig.VERSION_NAME, + BuildConfig.VERSION_CODE + ) + ) } } } + +@OptIn(ExperimentalAnimationApi::class) +@Composable +private fun DialogComposable(state: IInitialisation.State?) { + CrossFadeOverlay( + modifier = Modifier.clickable {}, + visible = state is IInitialisation.State.Error + ) { + 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 as? IInitialisation.State.Error)?.exception + ) + } + } +} + +@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) + } + } + } +} \ 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 index 589043c..4d9d250 100644 --- 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 @@ -1,25 +1,35 @@ package com.pixelized.biblib.ui.viewmodel.initialisation -import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData interface IInitialisation { - @Composable - fun LoadApplication(content: @Composable (State) -> Unit) + val state: LiveData - @Composable - fun LoadBook(content: @Composable (State) -> Unit) + 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 state: State = State.Loading) : IInitialisation { - @Composable - override fun LoadApplication(content: (State) -> Unit) = content(state) - @Composable - override fun LoadBook(content: (State) -> Unit) = content(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 index 64abc53..f3534a7 100644 --- 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 @@ -1,7 +1,9 @@ package com.pixelized.biblib.ui.viewmodel.initialisation -import androidx.compose.runtime.* +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 @@ -11,7 +13,10 @@ 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() @@ -20,33 +25,37 @@ class InitialisationViewModel : ViewModel(), IInitialisation { private val client: IBibLibClient by inject() private val apiCache: IAPICacheRepository by inject() - @Composable - override fun LoadApplication(content: @Composable (IInitialisation.State) -> Unit) { - val state: MutableState = remember { mutableStateOf(Initial) } + private val _state = MutableLiveData() + override val state: LiveData get() = _state - LaunchedEffect(key1 = "LoadApplication") { - state.value = Loading + override fun loadApplication() { + viewModelScope.launch(Dispatchers.IO) { + _state.postValue(Initial) + _state.postValue(Loading) delay(2000) - - val loggedIn = loginWithGoogle() || loginWithCredential() - if (loggedIn) { - loadNewBooks() && loadAllBooks() + try { + val loggedIn = loginWithGoogle() || loginWithCredential() + if (loggedIn) { + loadNewBooks() && loadAllBooks() + } + _state.postValue(Finished(needLogin = loggedIn.not())) + } catch (exception: Exception) { + _state.postValue(Error(exception)) } - state.value = Finished(needLogin = loggedIn.not()) } - - content(state.value) } - @Composable - override fun LoadBook(content: (IInitialisation.State) -> Unit) { - val state: MutableState = remember { mutableStateOf(Initial) } - LaunchedEffect(key1 = "LoadBook") { - state.value = Loading - loadNewBooks() && loadAllBooks() - state.value = Finished(needLogin = false) + 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)) + } } - content(state.value) } private suspend fun loginWithGoogle(): Boolean { @@ -104,9 +113,6 @@ class InitialisationViewModel : ViewModel(), IInitialisation { private suspend fun loadAllBooks(): Boolean { client.service.list().let { response -> - // TODO: useless isn't it ? - apiCache.list = response - val factory = BookFactory() val books = response.data?.map { dto -> factory.fromListResponseToBook(dto, false) } books?.let { data -> bookRepository.update(data) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cff90d3..4f77ddd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,16 +9,17 @@ SEND Sign in with Google - Entering the Imperial Library of Trantor. - Oops! + Entering the Imperial Library of Trantor. + Entering the Imperial Library of Trantor. + Authentication successful + Library successfully loaded Sign in to BibLib Login Password Remember my credential - Authentication successful Rating Language