diff --git a/.idea/misc.xml b/.idea/misc.xml index 6412fe1..5a9794c 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -96,6 +96,17 @@ + + + + + + + + + + + diff --git a/app/src/main/java/com/pixelized/biblib/repository/credential/CredentialRepository.kt b/app/src/main/java/com/pixelized/biblib/repository/credential/CredentialRepository.kt index f99b7bc..0b5d1bc 100644 --- a/app/src/main/java/com/pixelized/biblib/repository/credential/CredentialRepository.kt +++ b/app/src/main/java/com/pixelized/biblib/repository/credential/CredentialRepository.kt @@ -4,7 +4,7 @@ import android.app.Application import android.content.Context import android.content.SharedPreferences import androidx.core.content.edit -import com.pixelized.biblib.ui.viewmodel.AuthenticationViewModel +import com.pixelized.biblib.ui.viewmodel.authentication.AuthenticationViewModel class CredentialRepository(application: Application) : ICredentialRepository { private val preferences = diff --git a/app/src/main/java/com/pixelized/biblib/repository/googlesignin/GoogleSingInRepository.kt b/app/src/main/java/com/pixelized/biblib/repository/googlesignin/GoogleSingInRepository.kt index 4741e35..3fc4785 100644 --- a/app/src/main/java/com/pixelized/biblib/repository/googlesignin/GoogleSingInRepository.kt +++ b/app/src/main/java/com/pixelized/biblib/repository/googlesignin/GoogleSingInRepository.kt @@ -1,56 +1,21 @@ package com.pixelized.biblib.repository.googlesignin import android.app.Application -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import com.google.android.gms.auth.api.signin.GoogleSignIn -import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.auth.api.signin.GoogleSignInClient import com.google.android.gms.auth.api.signin.GoogleSignInOptions -import com.google.android.gms.common.api.ApiException import com.pixelized.biblib.R -import com.pixelized.biblib.utils.exception.MissingTokenException class GoogleSingInRepository(application: Application) : IGoogleSingInRepository { - override val googleSignInOption: GoogleSignInOptions by lazy { + override val option: GoogleSignInOptions by lazy { GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestIdToken(application.getString(R.string.biblib_server_id)) .requestEmail() .build() } - override val googleSignIn: GoogleSignInClient by lazy { - GoogleSignIn.getClient(application, googleSignInOption) - } - - @Composable - override fun prepareLoginWithGoogle(): IGoogleSingInRepository.Request { - val result = remember { - mutableStateOf( - IGoogleSingInRepository.AuthenticationState.Initial - ) - } - val launcher = rememberLauncherForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { - try { - val task = GoogleSignIn.getSignedInAccountFromIntent(it.data) - val account: GoogleSignInAccount? = task.getResult(ApiException::class.java) - val idToken = account?.idToken ?: throw MissingTokenException() - result.value = IGoogleSingInRepository.AuthenticationState.Connect(idToken) - } catch (exception: Exception) { - result.value = IGoogleSingInRepository.AuthenticationState.Error(exception) - } - } - return IGoogleSingInRepository.Request(result, launcher) - } - - override fun loginWithGoogle(request: IGoogleSingInRepository.Request) { - request.result.value = IGoogleSingInRepository.AuthenticationState.Loading - request.launcher.launch(googleSignIn.signInIntent) + override val client: GoogleSignInClient by lazy { + GoogleSignIn.getClient(application, option) } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/repository/googlesignin/IGoogleSingInRepository.kt b/app/src/main/java/com/pixelized/biblib/repository/googlesignin/IGoogleSingInRepository.kt index 7d4ea62..9b1b8b3 100644 --- a/app/src/main/java/com/pixelized/biblib/repository/googlesignin/IGoogleSingInRepository.kt +++ b/app/src/main/java/com/pixelized/biblib/repository/googlesignin/IGoogleSingInRepository.kt @@ -1,30 +1,9 @@ package com.pixelized.biblib.repository.googlesignin -import android.content.Intent -import androidx.activity.result.ActivityResultLauncher -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState import com.google.android.gms.auth.api.signin.GoogleSignInClient import com.google.android.gms.auth.api.signin.GoogleSignInOptions interface IGoogleSingInRepository { - val googleSignInOption: GoogleSignInOptions - val googleSignIn: GoogleSignInClient - - @Composable - fun prepareLoginWithGoogle(): Request - - fun loginWithGoogle(request: Request) - - data class Request( - val result: MutableState, - val launcher: ActivityResultLauncher, - ) - - sealed class AuthenticationState { - object Initial : AuthenticationState() - object Loading : AuthenticationState() - data class Connect(val token: String) : AuthenticationState() - data class Error(val exception: Exception) : AuthenticationState() - } + val option: GoogleSignInOptions + val client: GoogleSignInClient } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/MainActivity.kt b/app/src/main/java/com/pixelized/biblib/ui/MainActivity.kt index 7ee46aa..87b91a5 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/MainActivity.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/MainActivity.kt @@ -9,15 +9,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.lifecycle.viewmodel.compose.viewModel -import com.google.android.gms.auth.api.signin.GoogleSignIn -import com.google.android.gms.auth.api.signin.GoogleSignInOptions -import com.pixelized.biblib.R 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.NavigationViewModel -import com.pixelized.biblib.ui.viewmodel.NavigationViewModel.Screen +import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel +import com.pixelized.biblib.ui.viewmodel.navigation.INavigation.Screen class MainActivity : ComponentActivity() { private val navigationViewModel: NavigationViewModel by viewModels() @@ -46,9 +43,9 @@ fun ContentComposable() { Crossfade(targetState = main) { when (it) { - is Screen.SplashScreen -> SplashScreenComposable(viewModel()) - is Screen.LoginScreen -> LoginScreenComposable(viewModel(), viewModel()) - is Screen.MainScreen -> MainScreenComposable(viewModel()) + is Screen.SplashScreen -> SplashScreenComposable() + is Screen.LoginScreen -> LoginScreenComposable() + is Screen.MainScreen -> MainScreenComposable() } } } diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/items/WaitingComposable.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/items/WaitingComposable.kt index 58c1826..237c5ff 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/composable/items/WaitingComposable.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/composable/items/WaitingComposable.kt @@ -1,10 +1,9 @@ package com.pixelized.biblib.ui.composable.items import androidx.compose.animation.Crossfade -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* import androidx.compose.material.Card import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.MaterialTheme @@ -12,6 +11,8 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -53,25 +54,42 @@ fun WaitingComposable( targetState = visible ) { if (it) { - Card(elevation = 8.dp) { - Column( + Box( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .clickable { } + ) { + Box( modifier = Modifier - .width(200.dp) - .padding(16.dp) + .fillMaxWidth() + .fillMaxHeight() + .alpha(.25f) + .background(Color.Black) + ) + Card( + elevation = 8.dp, + modifier = Modifier.align(Alignment.Center) ) { - CircularProgressIndicator( + Column( modifier = Modifier - .align(Alignment.CenterHorizontally) + .width(200.dp) .padding(16.dp) - ) - if (message?.isNotEmpty() == true) { - val typography = MaterialTheme.typography - Text( - modifier = Modifier.align(Alignment.CenterHorizontally), - style = typography.body1, - textAlign = TextAlign.Center, - text = message + ) { + CircularProgressIndicator( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(16.dp) ) + if (message?.isNotEmpty() == true) { + val typography = MaterialTheme.typography + Text( + modifier = Modifier.align(Alignment.CenterHorizontally), + style = typography.body1, + textAlign = TextAlign.Center, + text = message + ) + } } } } diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/pages/HomePageComposable.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/pages/HomePageComposable.kt index 2511670..ed8cb28 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/composable/pages/HomePageComposable.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/composable/pages/HomePageComposable.kt @@ -12,8 +12,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.pixelized.biblib.ui.composable.items.BookThumbnailComposable import com.pixelized.biblib.ui.theme.BibLibTheme -import com.pixelized.biblib.ui.viewmodel.NavigationViewModel -import com.pixelized.biblib.ui.viewmodel.NavigationViewModel.Page.Detail +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 @@ -21,12 +22,13 @@ import com.pixelized.biblib.utils.mock.BookThumbnailMock @Composable fun HomePageComposablePreview() { BibLibTheme { - HomePageComposable(null) + val navigation = NavigationViewModel() + HomePageComposable(navigation) } } @Composable -fun HomePageComposable(navigationViewModel: NavigationViewModel?) { +fun HomePageComposable(navigation: INavigation) { val mock = BookThumbnailMock() LazyColumn( contentPadding = PaddingValues(16.dp), @@ -41,7 +43,7 @@ fun HomePageComposable(navigationViewModel: NavigationViewModel?) { ) { item -> // TODO: val bookMock = BookMock().let { it.books[item.id] ?: it.book } - navigationViewModel?.navigateTo(Detail(bookMock)) + navigation.navigateTo(Detail(bookMock)) } } } diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/screen/LoginScreenComposable.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/screen/LoginScreenComposable.kt index 6d914c3..1ae21f8 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 @@ -1,7 +1,6 @@ package com.pixelized.biblib.ui.composable.screen -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -29,142 +28,140 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel import com.pixelized.biblib.R -import com.pixelized.biblib.repository.googlesignin.IGoogleSingInRepository.AuthenticationState.Loading import com.pixelized.biblib.ui.composable.items.WaitingComposable import com.pixelized.biblib.ui.theme.BibLibTheme -import com.pixelized.biblib.ui.viewmodel.AuthenticationViewModel -import com.pixelized.biblib.ui.viewmodel.NavigationViewModel +import com.pixelized.biblib.ui.viewmodel.authentication.AuthenticationViewModel +import com.pixelized.biblib.ui.viewmodel.authentication.IAuthentication +import com.pixelized.biblib.ui.viewmodel.navigation.INavigation +import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel -@Preview -@Composable -fun LoginScreenComposablePreview() { - BibLibTheme { - val navigationViewModel = NavigationViewModel() - val authenticationViewModel = AuthenticationViewModel() - LoginScreenComposable(navigationViewModel, authenticationViewModel) - } -} -@OptIn(ExperimentalAnimationApi::class) @Composable fun LoginScreenComposable( - navigationViewModel: NavigationViewModel, - authenticationViewModel: AuthenticationViewModel, + navigation: INavigation = viewModel(), + authentication: IAuthentication = viewModel(), ) { Box( modifier = Modifier .fillMaxWidth() .fillMaxHeight() ) { - val loginWithGoogleRequest = authenticationViewModel.prepareLoginWithGoogle() - val typography = MaterialTheme.typography - - Column( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - .verticalScroll(rememberScrollState()) - .padding(16.dp) - ) { - Spacer(modifier = Modifier.weight(1f)) - - Text( - modifier = Modifier - .padding(vertical = 16.dp) - .align(alignment = Alignment.CenterHorizontally), - style = typography.h4, - text = stringResource(id = R.string.welcome_sign_in) - ) - - Spacer(modifier = Modifier.weight(1f)) - - val focusRequester = remember { FocusRequester() } - val localFocus = LocalFocusManager.current - LoginField( - viewModel = authenticationViewModel, - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 16.dp), - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), - keyboardActions = KeyboardActions { focusRequester.requestFocus() } - ) - PasswordField( - viewModel = authenticationViewModel, - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 16.dp) - .focusRequester(focusRequester), - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions { localFocus.clearFocus() } - ) - CredentialRemember( - viewModel = authenticationViewModel, - modifier = Modifier - .height(48.dp) - .padding(bottom = 16.dp) - ) - Row( - modifier = Modifier - .padding(bottom = 16.dp) - .align(Alignment.End) - ) { - Button( - modifier = Modifier.padding(end = 8.dp), - colors = outlinedButtonColors(), - onClick = { - authenticationViewModel.register() - }) { - Text(text = stringResource(id = R.string.action_register)) - } - Button(onClick = { - authenticationViewModel.login() - }) { - Text(text = stringResource(id = R.string.action_login)) - } - } - - Spacer(modifier = Modifier.weight(2f)) - - Button( - modifier = Modifier.fillMaxWidth(), - colors = outlinedButtonColors(), - onClick = { - authenticationViewModel.loginWithGoogle(loginWithGoogleRequest) - }) { - Image( - modifier = Modifier.padding(end = 8.dp), - painter = painterResource(id = R.drawable.ic_google), contentDescription = "" - ) - Text(text = stringResource(id = R.string.action_google_sign_in)) - } - } - - var waiting: Boolean by remember { mutableStateOf(false) } - waiting = when (loginWithGoogleRequest.result.value) { - is Loading -> true - else -> false - } - WaitingComposable( - modifier = Modifier.align(Alignment.Center), - visible = waiting, - message = stringResource(id = R.string.loading) - ) + authentication.PrepareLoginWithGoogle() + LoginScreenContentComposable(navigation, authentication) + LoginScreenWaitingComposable(authentication) } } +@Composable +private fun LoginScreenContentComposable( + navigation: INavigation, + authentication: IAuthentication, +) { + val typography = MaterialTheme.typography + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .verticalScroll(rememberScrollState()) + .padding(16.dp) + ) { + Spacer(modifier = Modifier.weight(1f)) + + Text( + modifier = Modifier + .padding(vertical = 16.dp) + .align(alignment = Alignment.CenterHorizontally), + style = typography.h4, + text = stringResource(id = R.string.welcome_sign_in) + ) + + Spacer(modifier = Modifier.weight(1f)) + + val focusRequester = remember { FocusRequester() } + val localFocus = LocalFocusManager.current + LoginField( + authentication = authentication, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions { focusRequester.requestFocus() } + ) + PasswordField( + authentication = authentication, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp) + .focusRequester(focusRequester), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions { localFocus.clearFocus() } + ) + CredentialRemember( + authentication = authentication, + modifier = Modifier + .height(48.dp) + .padding(bottom = 16.dp) + ) + Row( + modifier = Modifier + .padding(bottom = 16.dp) + .align(Alignment.End) + ) { + Button( + modifier = Modifier.padding(end = 8.dp), + colors = outlinedButtonColors(), + onClick = { + authentication.register() + }) { + Text(text = stringResource(id = R.string.action_register)) + } + Button(onClick = { + authentication.login() + }) { + Text(text = stringResource(id = R.string.action_login)) + } + } + + Spacer(modifier = Modifier.weight(2f)) + + Button( + modifier = Modifier.fillMaxWidth(), + colors = outlinedButtonColors(), + onClick = { + authentication.loginWithGoogle() + }) { + Image( + modifier = Modifier.padding(end = 8.dp), + painter = painterResource(id = R.drawable.ic_google), contentDescription = "" + ) + Text(text = stringResource(id = R.string.action_google_sign_in)) + } + } +} + +@Composable +private fun LoginScreenWaitingComposable(authentication: IAuthentication) { + val state = authentication.state.observeAsState() + WaitingComposable( + visible = state.value is IAuthentication.State.Loading, + message = stringResource(id = R.string.loading) + ) +} + @Composable private fun LoginField( - viewModel: AuthenticationViewModel, + authentication: IAuthentication, modifier: Modifier = Modifier, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions(), ) { - val login: State = viewModel.login.observeAsState() + val login: State = authentication.login.observeAsState() TextField( modifier = modifier, value = login.value ?: "", - onValueChange = { viewModel.updateLoginField(it) }, + onValueChange = { authentication.updateLoginField(it) }, label = { Text(text = stringResource(id = R.string.authentication_login)) }, colors = outlinedTextFieldColors(), maxLines = 1, @@ -176,17 +173,17 @@ private fun LoginField( @Composable private fun PasswordField( - viewModel: AuthenticationViewModel, + authentication: IAuthentication, modifier: Modifier = Modifier, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions(), ) { - val password = viewModel.password.observeAsState() + val password = authentication.password.observeAsState() var passwordVisibility by remember { mutableStateOf(false) } TextField( modifier = modifier, value = password.value ?: "", - onValueChange = { viewModel.updatePasswordField(it) }, + onValueChange = { authentication.updatePasswordField(it) }, label = { Text(text = stringResource(id = R.string.authentication_password)) }, colors = outlinedTextFieldColors(), maxLines = 1, @@ -206,10 +203,15 @@ private fun PasswordField( } @Composable -private fun CredentialRemember(viewModel: AuthenticationViewModel, modifier: Modifier = Modifier) { - val credential = viewModel.rememberCredential.observeAsState() +private fun CredentialRemember( + authentication: IAuthentication, + modifier: Modifier = Modifier +) { + val credential = authentication.rememberCredential.observeAsState() Row(modifier = modifier.clickable { - viewModel.updateRememberCredential(rememberCredential = credential.value?.not() ?: false) + authentication.updateRememberCredential( + rememberCredential = credential.value?.not() ?: false + ) }) { Checkbox( modifier = Modifier.align(Alignment.CenterVertically), @@ -223,4 +225,24 @@ private fun CredentialRemember(viewModel: AuthenticationViewModel, modifier: Mod text = stringResource(id = R.string.authentication_credential_remember) ) } +} + +@Preview +@Composable +fun LoginScreenComposablePreview() { + BibLibTheme { + val navigationViewModel = INavigation.Mock() + val authenticationViewModel = IAuthentication.Mock() + LoginScreenComposable(navigationViewModel, authenticationViewModel) + } +} + +@Preview +@Composable +fun LoginScreenComposableWaitingPreview() { + BibLibTheme { + val navigationViewModel = INavigation.Mock() + val authenticationViewModel = IAuthentication.Mock(true) + LoginScreenComposable(navigationViewModel, authenticationViewModel) + } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/screen/MainScreenComposable.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/screen/MainScreenComposable.kt index 792986a..197b5be 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/composable/screen/MainScreenComposable.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/composable/screen/MainScreenComposable.kt @@ -11,28 +11,28 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +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.NavigationViewModel -import com.pixelized.biblib.ui.viewmodel.NavigationViewModel.Page +import com.pixelized.biblib.ui.viewmodel.navigation.INavigation +import com.pixelized.biblib.ui.viewmodel.navigation.INavigation.Page +import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel @Preview @Composable fun ToolbarComposableDarkPreview() { - val viewModel = NavigationViewModel() BibLibTheme(darkTheme = false) { - ToolbarComposable(navigationViewModel = viewModel) + ToolbarComposable(navigation = INavigation.Mock()) } } @Preview @Composable fun ToolbarComposableLightPreview() { - val viewModel = NavigationViewModel() BibLibTheme(darkTheme = true) { - ToolbarComposable(navigationViewModel = viewModel) + ToolbarComposable(navigation = INavigation.Mock()) } } @@ -40,27 +40,26 @@ fun ToolbarComposableLightPreview() { @Composable fun MainScreenComposablePreview() { BibLibTheme { - val viewModel = NavigationViewModel() - MainScreenComposable(viewModel) + MainScreenComposable(INavigation.Mock(page = Page.HomePage)) } } @Composable fun MainScreenComposable( - navigationViewModel: NavigationViewModel + navigation: INavigation = viewModel() ) { - val page by navigationViewModel.page.observeAsState() + val page by navigation.page.observeAsState() LaunchedEffect(key1 = "MainScreen", block = { - navigationViewModel.navigateTo(Page.HomePage) + navigation.navigateTo(Page.HomePage) }) Scaffold( - topBar = { ToolbarComposable(navigationViewModel) }, + topBar = { ToolbarComposable(navigation) }, ) { Crossfade(targetState = page) { when (it) { - is Page.HomePage -> HomePageComposable(navigationViewModel) + is Page.HomePage -> HomePageComposable(navigation) is Page.Detail -> DetailPageComposable(it.book) } } @@ -68,16 +67,16 @@ fun MainScreenComposable( } @Composable -fun ToolbarComposable(navigationViewModel: NavigationViewModel) { +fun ToolbarComposable(navigation: INavigation) { TopAppBar( title = { Text(stringResource(id = R.string.app_name)) }, - navigationIcon = { NavigationIcon(navigationViewModel) } + navigationIcon = { NavigationIcon(navigation) } ) } @Composable -fun NavigationIcon(navigationViewModel: NavigationViewModel) { - val page: Page? by navigationViewModel.page.observeAsState() +fun NavigationIcon(navigation: INavigation) { + val page: Page? by navigation.page.observeAsState() Crossfade(targetState = page) { when (it) { @@ -88,7 +87,7 @@ fun NavigationIcon(navigationViewModel: NavigationViewModel) { ) } else -> IconButton(onClick = { - navigationViewModel.navigateBack() + navigation.navigateBack() }) { Icon( imageVector = Icons.Sharp.ArrowBack, diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/screen/SplashScreenComposable.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/screen/SplashScreenComposable.kt index 4378a33..9d163e4 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 @@ -11,8 +11,10 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.viewmodel.compose.viewModel import com.pixelized.biblib.ui.theme.BibLibTheme -import com.pixelized.biblib.ui.viewmodel.NavigationViewModel +import com.pixelized.biblib.ui.viewmodel.navigation.INavigation +import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -21,14 +23,13 @@ import kotlinx.coroutines.launch @Composable fun SplashScreenComposablePreview() { BibLibTheme { - val viewModel = NavigationViewModel() - SplashScreenComposable(viewModel) + SplashScreenComposable(INavigation.Mock()) } } @Composable fun SplashScreenComposable( - navigationViewModel: NavigationViewModel + navigation: INavigation = viewModel() ) { val typography = MaterialTheme.typography @@ -48,7 +49,7 @@ fun SplashScreenComposable( LaunchedEffect(key1 = "loading", block = { coroutineScope.launch { delay(1000) - navigationViewModel.navigateTo(NavigationViewModel.Screen.LoginScreen) + navigation.navigateTo(INavigation.Screen.LoginScreen) } }) } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/NavigationViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/NavigationViewModel.kt deleted file mode 100644 index c3e544b..0000000 --- a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/NavigationViewModel.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.pixelized.biblib.ui.viewmodel - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.pixelized.biblib.data.ui.BookUio -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import java.util.* - -class NavigationViewModel : ViewModel() { - - private val stack = Stack() - - private val _screen = MutableLiveData(Screen.SplashScreen) - val screen: LiveData get() = _screen - - private val _page = MutableLiveData(Page.HomePage) - val page: LiveData get() = _page - - fun navigateTo(screen: Screen): Boolean { - _screen.postValue(screen) - return true - } - - fun navigateTo(page: Page): Boolean { - _page.postValue(page) - stack.push(page) - return true - } - - fun navigateBack() : Boolean { - stack.pop() - return if (stack.empty()) { - false - } else { - _page.postValue(stack.peek()) - true - } - } - - sealed class Screen { - object SplashScreen : Screen() - object MainScreen : Screen() - object LoginScreen : Screen() - } - - sealed class Page { - object HomePage : Page() - data class Detail(val book: BookUio) : Page() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/AuthenticationViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/AuthenticationViewModel.kt similarity index 52% rename from app/src/main/java/com/pixelized/biblib/ui/viewmodel/AuthenticationViewModel.kt rename to app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/AuthenticationViewModel.kt index 963de79..4548f80 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/AuthenticationViewModel.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/AuthenticationViewModel.kt @@ -1,32 +1,48 @@ -package com.pixelized.biblib.ui.viewmodel +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 import androidx.compose.runtime.Composable import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel 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.data.network.query.AuthLoginQuery import com.pixelized.biblib.injection.inject import com.pixelized.biblib.network.client.IBibLibClient 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.utils.exception.MissingTokenException import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch -class AuthenticationViewModel : ViewModel() { +class AuthenticationViewModel : ViewModel(), IAuthentication { + private val credentialRepository: ICredentialRepository by inject() private val googleSignIn: IGoogleSingInRepository by inject() private val client: IBibLibClient by inject() + private var launcher: ActivityResultLauncher? = null + + private val _state = MutableLiveData(State.Initial) + override val state: LiveData get() = _state + private val _login = MutableLiveData() - val login: LiveData get() = _login + override val login: LiveData get() = _login private val _password = MutableLiveData() - val password: LiveData get() = _password + override val password: LiveData get() = _password private val _rememberCredential = MutableLiveData() - val rememberCredential: LiveData get() = _rememberCredential + override val rememberCredential: LiveData get() = _rememberCredential init { viewModelScope.launch(Dispatchers.Main) { @@ -36,15 +52,15 @@ class AuthenticationViewModel : ViewModel() { } } - fun updateLoginField(login: String) { + override fun updateLoginField(login: String) { _login.postValue(login) } - fun updatePasswordField(password: String) { + override fun updatePasswordField(password: String) { _password.postValue(password) } - fun updateRememberCredential(rememberCredential: Boolean) { + override fun updateRememberCredential(rememberCredential: Boolean) { _rememberCredential.postValue(rememberCredential) viewModelScope.launch { credentialRepository.rememberCredential = rememberCredential @@ -55,11 +71,15 @@ class AuthenticationViewModel : ViewModel() { } } - fun register() { - + override fun register() { + viewModelScope.launch { + _state.postValue(State.Loading) + delay(3000) + _state.postValue(State.Initial) + } } - fun login() { + override fun login() { viewModelScope.launch(Dispatchers.IO) { if (rememberCredential.value == true) { credentialRepository.login = login.value @@ -77,11 +97,25 @@ class AuthenticationViewModel : ViewModel() { } @Composable - fun prepareLoginWithGoogle(): IGoogleSingInRepository.Request = - googleSignIn.prepareLoginWithGoogle() + override fun PrepareLoginWithGoogle() { + launcher = rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + try { + val task = GoogleSignIn.getSignedInAccountFromIntent(it.data) + val account: GoogleSignInAccount? = task.getResult(ApiException::class.java) + val idToken = account?.idToken ?: throw MissingTokenException() + _state.postValue(State.Connect(idToken)) + } catch (exception: Exception) { + _state.postValue(State.Error(exception)) + } + } + } - fun loginWithGoogle(request: IGoogleSingInRepository.Request) = - googleSignIn.loginWithGoogle(request) + override fun loginWithGoogle() { + _state.postValue(State.Loading) + launcher?.launch(googleSignIn.client.signInIntent) + } companion object { const val SHARED_PREF = "BIB_LIB_SHARED_PREF" diff --git a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/IAuthentication.kt b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/IAuthentication.kt new file mode 100644 index 0000000..402e506 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/authentication/IAuthentication.kt @@ -0,0 +1,48 @@ +package com.pixelized.biblib.ui.viewmodel.authentication + +import androidx.compose.runtime.Composable +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData + +interface IAuthentication { + val state: LiveData + val login: LiveData + val password: LiveData + val rememberCredential: LiveData + + fun updateLoginField(login: String) + fun updatePasswordField(password: String) + fun updateRememberCredential(rememberCredential: Boolean) + + fun register() + fun login() + + @Composable + fun PrepareLoginWithGoogle() + fun loginWithGoogle() + + sealed class State { + object Initial : State() + object Loading : State() + data class Connect(val token: String) : State() + data class Error(val exception: Exception) : State() + } + + class Mock(waiting: Boolean = false) : IAuthentication { + override val state: LiveData = + MutableLiveData(if (waiting) State.Loading else State.Initial) + override val login: LiveData = MutableLiveData("") + override val password: LiveData = MutableLiveData("") + override val rememberCredential: LiveData = MutableLiveData(true) + + override fun updateLoginField(login: String) = Unit + override fun updatePasswordField(password: String) = Unit + override fun updateRememberCredential(rememberCredential: Boolean) = Unit + override fun register() = Unit + override fun login() = Unit + + @Composable + override fun PrepareLoginWithGoogle() = Unit + override fun loginWithGoogle() = Unit + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/navigation/INavigation.kt b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/navigation/INavigation.kt new file mode 100644 index 0000000..6e8f176 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/navigation/INavigation.kt @@ -0,0 +1,35 @@ +package com.pixelized.biblib.ui.viewmodel.navigation + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.pixelized.biblib.data.ui.BookUio + +interface INavigation { + val screen: LiveData + val page: LiveData + + fun navigateTo(screen: Screen): Boolean + fun navigateTo(page: Page): Boolean + + fun navigateBack(): Boolean + + sealed class Screen { + object SplashScreen : Screen() + object MainScreen : Screen() + object LoginScreen : Screen() + } + + sealed class Page { + object HomePage : Page() + data class Detail(val book: BookUio) : Page() + } + + class Mock(screen: Screen = Screen.SplashScreen, page: Page = Page.HomePage) : INavigation { + override val screen: LiveData = MutableLiveData(screen) + override val page: LiveData = MutableLiveData(page) + + override fun navigateTo(screen: Screen): Boolean = false + override fun navigateTo(page: Page): Boolean = false + override fun navigateBack(): Boolean = false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/navigation/NavigationViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/navigation/NavigationViewModel.kt new file mode 100644 index 0000000..c630c93 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/navigation/NavigationViewModel.kt @@ -0,0 +1,41 @@ +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 java.util.* + +class NavigationViewModel : ViewModel(), INavigation { + + private val stack = Stack() + + private val _screen = MutableLiveData(Screen.SplashScreen) + override val screen: LiveData get() = _screen + + private val _page = MutableLiveData(Page.HomePage) + override val page: LiveData get() = _page + + override fun navigateTo(screen: Screen): Boolean { + _screen.postValue(screen) + return true + } + + override fun navigateTo(page: Page): Boolean { + _page.postValue(page) + stack.push(page) + return true + } + + override fun navigateBack(): Boolean { + stack.pop() + return if (stack.empty()) { + false + } else { + _page.postValue(stack.peek()) + true + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/utils/alias/Mock.kt b/app/src/main/java/com/pixelized/biblib/utils/alias/Mock.kt new file mode 100644 index 0000000..82ffb09 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/utils/alias/Mock.kt @@ -0,0 +1,3 @@ +package com.pixelized.biblib.utils.alias + +fun mock(): Nothing = throw NotImplementedError() \ No newline at end of file