Add detail screen.

This commit is contained in:
Thomas Andres Gomez 2022-04-22 17:02:40 +02:00
parent de64718e10
commit 0f2b50f42e
18 changed files with 621 additions and 280 deletions

View file

@ -89,7 +89,7 @@ dependencies {
implementation 'androidx.activity:activity-compose:1.4.0'
// Android Compose
implementation "androidx.compose.ui:ui:1.2.0-alpha03"
implementation "androidx.compose.ui:ui:1.2.0-alpha08"
implementation "androidx.compose.material:material:1.1.1"
implementation "androidx.compose.runtime:runtime-livedata:1.1.1"
implementation "androidx.compose.ui:ui-tooling-preview:1.1.1"

View file

@ -7,16 +7,14 @@ import androidx.activity.viewModels
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import com.google.accompanist.insets.ProvideWindowInsets
import com.google.accompanist.systemuicontroller.SystemUiController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.pixelized.biblib.ui.composable.SystemThemeColor
import com.pixelized.biblib.ui.screen.launch.LauncherViewModel
import com.pixelized.biblib.ui.navigation.screen.ScreenNavHost
import com.pixelized.biblib.ui.navigation.screen.Screen
import com.pixelized.biblib.ui.screen.launch.LauncherViewModel
import com.pixelized.biblib.ui.theme.BibLibTheme
import dagger.hilt.android.AndroidEntryPoint
@ -35,7 +33,7 @@ class MainActivity : ComponentActivity() {
// splashscreen management
installSplashScreen().apply {
setKeepOnScreenCondition {
launcherViewModel.isLoading.value
launcherViewModel.isLoading
}
}
@ -44,18 +42,18 @@ class MainActivity : ComponentActivity() {
BibLibTheme {
ProvideWindowInsets {
Surface(color = MaterialTheme.colors.background) {
// handle the system colors
// Handle the system colors
val systemUiController: SystemUiController = rememberSystemUiController()
SystemThemeColor(
systemUiController = systemUiController,
statusDarkIcons = isSystemInDarkTheme().not(),
navigationDarkIcons = isSystemInDarkTheme().not(),
)
// handle the main composition
val loading by launcherViewModel.isLoading
if (loading.not()) {
// Handle the main Navigation
if (launcherViewModel.isLoading.not()) {
ScreenNavHost(
startDestination = Screen.Authentication
startDestination = launcherViewModel.startDestination
)
}
}

View file

@ -4,9 +4,10 @@ import androidx.compose.runtime.Composable
@Composable
fun AnimatedDelayer(
delay: Delay = Delay(),
content: @Composable AnimatedDelayerScope.() -> Unit
) {
val scope = AnimatedDelayerScope()
val scope = AnimatedDelayerScope(delay = delay)
scope.content()
}

View file

@ -1,5 +1,9 @@
package com.pixelized.biblib.ui.navigation.screen
import androidx.navigation.NamedNavArgument
import androidx.navigation.NavType
import androidx.navigation.navArgument
sealed class Screen(
val route: String,
) {
@ -12,12 +16,16 @@ sealed class Screen(
)
class BookDetail(id: Int) : Screen(
route = "$ROOT/$id"
route = "$ROUTE/$id"
) {
companion object {
const val ROOT = "detail"
private const val ROUTE = "detail"
const val ARG_BOOK_ID = "id"
const val route = "$ROOT/{$ARG_BOOK_ID}"
const val route = "$ROUTE/{$ARG_BOOK_ID}"
val arguments: List<NamedNavArgument> = listOf(
navArgument(ARG_BOOK_ID) { type = NavType.IntType }
)
}
}
}

View file

@ -34,9 +34,23 @@ fun ScreenNavHost(
composable(Screen.Home.route) {
HomeScreen()
}
composable(Screen.BookDetail.route) {
composable(
route = Screen.BookDetail.route,
arguments = Screen.BookDetail.arguments,
) {
DetailScreen()
}
}
}
}
fun NavHostController.navigateToHome() {
navigate(Screen.Home.route) {
launchSingleTop = true
popUpTo(0) { inclusive = true }
}
}
fun NavHostController.navigateToBookDetail(bookId: Int) {
navigate(Screen.BookDetail(id = bookId).route)
}

View file

@ -32,40 +32,46 @@ import androidx.navigation.NavHostController
import com.google.accompanist.insets.systemBarsPadding
import com.pixelized.biblib.R
import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.ui.composable.animation.AnimatedDelayer
import com.pixelized.biblib.ui.composable.animation.AnimatedOffset
import com.pixelized.biblib.ui.composable.StateUio
import com.pixelized.biblib.ui.composable.StateUioHandler
import com.pixelized.biblib.ui.composable.animation.AnimatedDelayer
import com.pixelized.biblib.ui.composable.animation.AnimatedOffset
import com.pixelized.biblib.ui.navigation.screen.LocalScreenNavHostController
import com.pixelized.biblib.ui.navigation.screen.Screen
import com.pixelized.biblib.ui.navigation.screen.navigateToHome
import com.pixelized.biblib.ui.screen.authentication.viewModel.AuthenticationFormViewModel
import com.pixelized.biblib.ui.screen.authentication.viewModel.AuthenticationViewModel
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.theme.color.GoogleColorPalette
import com.pixelized.biblib.utils.extention.bibLib
@Composable
fun AuthenticationScreen(
viewModel: AuthenticationViewModel = hiltViewModel(),
authenticationViewModel: AuthenticationViewModel = hiltViewModel(),
formViewModel: AuthenticationFormViewModel = hiltViewModel(),
) {
val navHostController = LocalScreenNavHostController.current
AuthenticationScreenContent(
login = viewModel.form.login,
password = viewModel.form.password,
rememberPassword = viewModel.form.remember,
login = formViewModel.form.login,
password = formViewModel.form.password,
rememberPassword = formViewModel.form.remember,
onLoginChange = {
viewModel.onLoginChange(it)
formViewModel.onLoginChange(it)
},
onPasswordChange = {
viewModel.onPasswordChange(it)
formViewModel.onPasswordChange(it)
},
onRememberPasswordChange = {
viewModel.onRememberChange(it)
formViewModel.onRememberChange(it)
},
onGoogleSignIn = {
viewModel.loginWithGoogle()
authenticationViewModel.loginWithGoogle()
},
onSignIn = {
viewModel.login()
authenticationViewModel.login(
login = formViewModel.form.login,
password = formViewModel.form.password,
)
},
onRegister = {
navHostController.navigateToRegister()
@ -74,7 +80,7 @@ fun AuthenticationScreen(
AuthenticationHandler(
onDismissRequest = {
if (it is StateUio.Failure) viewModel.dismissError()
if (it is StateUio.Failure) authenticationViewModel.dismissError()
},
onSuccess = {
navHostController.navigateToHome()
@ -308,10 +314,6 @@ private fun CredentialRemember(
//////////////////////////////////////
// region: Navigation Helper
private fun NavHostController.navigateToHome() {
navigate(Screen.Home.route) { popUpTo(0) { inclusive = true } }
}
private fun NavHostController.navigateToRegister() {
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(IBibLibClient.REGISTER_URL)))
}

View file

@ -1,220 +0,0 @@
package com.pixelized.biblib.ui.screen.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.*
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.common.api.ApiException
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.composable.StateUio
import com.pixelized.biblib.utils.exception.MissingGoogleTokenException
import com.pixelized.biblib.utils.exception.MissingTokenException
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class AuthenticationViewModel @Inject constructor(
private val credentialRepository: ICredentialRepository,
private val googleSignIn: IGoogleSingInRepository,
private val client: IBibLibClient,
) : ViewModel() {
private var launcher: ActivityResultLauncher<Intent>? = null
private var authenticationJob: Job? = null
private val _authenticationProcess = mutableStateOf<StateUio<Unit>?>(null)
val authenticationProcess: State<StateUio<Unit>?> get() = _authenticationProcess
private val _login: MutableState<String>
private val _password: MutableState<String>
private val _remember = mutableStateOf(credentialRepository.rememberCredential)
val form: AuthenticationFormUIO
get() = AuthenticationFormUIO(
login = _login,
password = _password,
remember = _remember,
)
init {
if (credentialRepository.rememberCredential) {
_login = mutableStateOf(credentialRepository.login ?: "")
_password = mutableStateOf(credentialRepository.password ?: "")
} else {
_login = mutableStateOf("")
_password = mutableStateOf("")
}
}
//////////////////////////////////////
// region: Login with BibLibClient
fun login(
login: String = _login.value,
password: String = _password.value,
) {
authenticationJob?.cancel()
authenticationJob = viewModelScope.launch(Dispatchers.IO) {
val query = AuthLoginQuery(username = login, password = password)
_authenticationProcess.value = StateUio.Progress()
try {
val response = client.service.login(query)
val idToken = response.token ?: throw MissingTokenException()
client.token = idToken
_authenticationProcess.value = StateUio.Success(Unit)
} catch (exception: Exception) {
Log.e("AuthenticationViewModel", exception.message, exception)
_authenticationProcess.value = StateUio.Failure(exception)
}
}
}
// endregion
//////////////////////////////////////
// region: Login with Google
@Composable
fun PrepareLoginWithGoogle() {
launcher = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
authenticationJob?.cancel()
authenticationJob = viewModelScope.launch(Dispatchers.IO) {
try {
val task = GoogleSignIn.getSignedInAccountFromIntent(it.data)
val account = task.getResult(ApiException::class.java)
val googleToken = account?.idToken ?: throw MissingGoogleTokenException()
val response = client.service.loginWithGoogle(googleToken)
val token = response.token ?: throw MissingTokenException()
client.token = token
_authenticationProcess.value = StateUio.Success(Unit)
} catch (exception: Exception) {
Log.e("AuthenticationViewModel", exception.message, exception)
_authenticationProcess.value = StateUio.Failure(exception)
}
}
}
}
fun loginWithGoogle() {
_authenticationProcess.value = StateUio.Progress()
launcher?.launch(googleSignIn.client.signInIntent)
}
// endregion
//////////////////////////////////////
// region: AutoLogin
@Composable
fun AutoLogin() {
LaunchedEffect(key1 = "AuthenticationViewModel AutoLogin") {
authenticationJob?.cancel()
authenticationJob = launch(Dispatchers.IO) {
_authenticationProcess.value = StateUio.Progress()
try {
autoLoginWithGoogle() || autologinWithCredential()
_authenticationProcess.value = StateUio.Success(Unit)
} catch (exception: Exception) {
_authenticationProcess.value = StateUio.Failure(exception)
}
}
}
}
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
}
}
// endregion
//////////////////////////////////////
// region: OnDataChange callback.
fun onLoginChange(login: String) {
// update login in the repository
if (_remember.value) {
credentialRepository.login = login
}
// update the UI State
_login.value = login
}
fun onPasswordChange(password: String) {
// update password in the repository
if (_remember.value) {
credentialRepository.password = password
}
// update the UI State
_password.value = password
}
fun onRememberChange(remember: Boolean) {
// save the remember state in the repository
credentialRepository.rememberCredential = remember
// update login & password in the repository
if (remember.not()) {
credentialRepository.login = null
credentialRepository.password = null
} else {
credentialRepository.login = _login.value
credentialRepository.password = _password.value
}
// update the UI State
_remember.value = remember
}
// endregion
//////////////////////////////////////
// region: Dialog
fun dismissError() {
_authenticationProcess.value = null
}
}

View file

@ -0,0 +1,69 @@
package com.pixelized.biblib.ui.screen.authentication.viewModel
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import com.pixelized.biblib.repository.credential.ICredentialRepository
import com.pixelized.biblib.ui.screen.authentication.AuthenticationFormUIO
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class AuthenticationFormViewModel @Inject constructor(
private val credentialRepository: ICredentialRepository,
) : ViewModel() {
private val _login: MutableState<String>
private val _password: MutableState<String>
private val _remember = mutableStateOf(credentialRepository.rememberCredential)
val form: AuthenticationFormUIO
get() = AuthenticationFormUIO(
login = _login,
password = _password,
remember = _remember,
)
init {
if (credentialRepository.rememberCredential) {
_login = mutableStateOf(credentialRepository.login ?: "")
_password = mutableStateOf(credentialRepository.password ?: "")
} else {
_login = mutableStateOf("")
_password = mutableStateOf("")
}
}
fun onLoginChange(login: String) {
// update login in the repository
if (_remember.value) {
credentialRepository.login = login
}
// update the UI State
_login.value = login
}
fun onPasswordChange(password: String) {
// update password in the repository
if (_remember.value) {
credentialRepository.password = password
}
// update the UI State
_password.value = password
}
fun onRememberChange(remember: Boolean) {
// save the remember state in the repository
credentialRepository.rememberCredential = remember
// update login & password in the repository
if (remember.not()) {
credentialRepository.login = null
credentialRepository.password = null
} else {
credentialRepository.login = _login.value
credentialRepository.password = _password.value
}
// update the UI State
_remember.value = remember
}
}

View file

@ -0,0 +1,97 @@
package com.pixelized.biblib.ui.screen.authentication.viewModel
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.*
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.common.api.ApiException
import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.network.data.query.AuthLoginQuery
import com.pixelized.biblib.repository.googleSignIn.IGoogleSingInRepository
import com.pixelized.biblib.ui.composable.StateUio
import com.pixelized.biblib.utils.exception.MissingGoogleTokenException
import com.pixelized.biblib.utils.exception.MissingTokenException
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class AuthenticationViewModel @Inject constructor(
private val googleSignIn: IGoogleSingInRepository,
private val client: IBibLibClient,
) : ViewModel() {
private var launcher: ActivityResultLauncher<Intent>? = null
private var authenticationJob: Job? = null
private val _authenticationProcess = mutableStateOf<StateUio<Unit>?>(null)
val authenticationProcess: State<StateUio<Unit>?> get() = _authenticationProcess
//////////////////////////////////////
// region: Login with BibLibClient
fun login(login: String, password: String) {
authenticationJob?.cancel()
authenticationJob = viewModelScope.launch(Dispatchers.IO) {
val query = AuthLoginQuery(username = login, password = password)
_authenticationProcess.value = StateUio.Progress()
try {
val response = client.service.login(query)
val idToken = response.token ?: throw MissingTokenException()
client.token = idToken
_authenticationProcess.value = StateUio.Success(Unit)
} catch (exception: Exception) {
Log.e("AuthenticationViewModel", exception.message, exception)
_authenticationProcess.value = StateUio.Failure(exception)
}
}
}
// endregion
//////////////////////////////////////
// region: Login with Google
@Composable
fun PrepareLoginWithGoogle() {
launcher = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
authenticationJob?.cancel()
authenticationJob = viewModelScope.launch(Dispatchers.IO) {
try {
val task = GoogleSignIn.getSignedInAccountFromIntent(it.data)
val account = task.getResult(ApiException::class.java)
val googleToken = account?.idToken ?: throw MissingGoogleTokenException()
val response = client.service.loginWithGoogle(googleToken)
val token = response.token ?: throw MissingTokenException()
client.token = token
_authenticationProcess.value = StateUio.Success(Unit)
} catch (exception: Exception) {
Log.e("AuthenticationViewModel", exception.message, exception)
_authenticationProcess.value = StateUio.Failure(exception)
}
}
}
}
fun loginWithGoogle() {
_authenticationProcess.value = StateUio.Progress()
launcher?.launch(googleSignIn.client.signInIntent)
}
// endregion
//////////////////////////////////////
// region: Dialog
fun dismissError() {
_authenticationProcess.value = null
}
// endregion
}

View file

@ -0,0 +1,69 @@
package com.pixelized.biblib.ui.screen.detail
import android.util.Log
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.biblib.model.book.Book
import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.network.factory.BookFactory
import com.pixelized.biblib.ui.composable.StateUio
import com.pixelized.biblib.ui.navigation.screen.Screen
import com.pixelized.biblib.ui.screen.home.common.uio.BookUio
import com.pixelized.biblib.utils.extention.capitalize
import com.pixelized.biblib.utils.extention.shortDate
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class BookDetailViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val client: IBibLibClient,
) : ViewModel() {
private val _state = mutableStateOf<StateUio<BookUio>>(StateUio.Progress())
val state: State<StateUio<BookUio>> get() = _state
val book: State<BookUio?> = derivedStateOf {
state.value.let { if (it is StateUio.Success<BookUio>) it.value else null }
}
init {
viewModelScope.launch(Dispatchers.IO) {
try {
val id = savedStateHandle.bookId
val book = getBookDetail(id = id)
_state.value = StateUio.Success(book)
} catch (exception: Exception) {
Log.e("BookDetailViewModel", exception.message, exception)
_state.value = StateUio.Failure(exception)
}
}
}
private suspend fun getBookDetail(id: Int): BookUio {
val factory = BookFactory()
val response = client.service.detail(id)
val book = factory.fromDetailResponseToBook(response)
return book.toUio()
}
private fun Book.toUio() = BookUio(
id = id,
title = title,
author = author.joinToString { it.name },
rating = rating?.toFloat() ?: 0.0f,
language = language?.displayLanguage?.capitalize() ?: "",
date = releaseDate.shortDate(),
series = series?.name,
description = synopsis ?: "",
)
private val SavedStateHandle.bookId: Int
get() = get<Int>(Screen.BookDetail.ARG_BOOK_ID) ?: error("")
}

View file

@ -2,22 +2,197 @@ package com.pixelized.biblib.ui.screen.detail
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.widget.TextView
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Send
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.text.HtmlCompat
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.biblib.ui.old.viewmodel.book.BooksViewModel
import com.google.accompanist.insets.systemBarsPadding
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.composable.animation.AnimatedDelayer
import com.pixelized.biblib.ui.composable.animation.AnimatedOffset
import com.pixelized.biblib.ui.composable.animation.Delay
import com.pixelized.biblib.ui.screen.home.common.uio.BookUio
import com.pixelized.biblib.ui.theme.BibLibTheme
@Composable
fun DetailScreen(
booksViewModel: BooksViewModel = hiltViewModel()
viewModel: BookDetailViewModel = hiltViewModel()
) {
Box {
val book by viewModel.book
book?.let {
DetailScreenContent(
book = it,
onSendClick = {},
)
}
}
}
@Composable
private fun DetailScreenContent() {
private fun DetailScreenContent(
book: BookUio,
onSendClick: () -> Unit,
) {
AnimatedDelayer(delay = Delay(300)) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.systemBarsPadding()
.padding(horizontal = 16.dp)
) {
AnimatedOffset(
modifier = Modifier
.padding(16.dp)
.align(Alignment.CenterHorizontally),
) {
Card(
modifier = Modifier
.size(200.dp, 320.dp)
.background(MaterialTheme.colors.surface),
) {
Box(contentAlignment = Alignment.Center) {
Image(
modifier = Modifier.size(64.dp),
painter = painterResource(id = R.drawable.ic_baseline_auto_stories_24),
contentDescription = null,
)
}
}
}
AnimatedOffset(
modifier = Modifier
.padding(16.dp)
.align(Alignment.CenterHorizontally),
) {
Button(
onClick = onSendClick,
) {
Icon(imageVector = Icons.Default.Send, contentDescription = "")
Spacer(modifier = Modifier.width(4.dp))
Text(text = stringResource(id = R.string.action_send))
}
}
AnimatedOffset(
modifier = Modifier
.align(alignment = Alignment.CenterHorizontally)
.padding(bottom = 4.dp),
) {
Text(
style = MaterialTheme.typography.h5,
text = book.title,
)
}
AnimatedOffset(
modifier = Modifier
.align(alignment = Alignment.CenterHorizontally)
.padding(bottom = 16.dp),
) {
Text(
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.h6,
text = book.author,
)
}
Row(modifier = Modifier.padding(bottom = 8.dp)) {
AnimatedOffset(modifier = Modifier.weight(1f)) {
TitleLabel(
title = stringResource(id = R.string.detail_rating),
label = book.rating.toString(),
)
}
AnimatedOffset(modifier = Modifier.weight(1f)) {
TitleLabel(
title = stringResource(id = R.string.detail_language),
label = book.language,
)
}
book.date?.let {
AnimatedOffset(modifier = Modifier.weight(1f)) {
TitleLabel(
title = stringResource(id = R.string.detail_release),
label = it,
)
}
}
}
AnimatedOffset {
Row(modifier = Modifier.padding(bottom = 16.dp)) {
TitleLabel(
title = stringResource(id = R.string.detail_series),
label = book.series ?: "-",
)
}
}
AnimatedOffset {
HtmlText(
html = book.description,
modifier = Modifier.padding(bottom = 16.dp)
)
}
}
}
}
@Composable
private fun TitleLabel(
title: String,
label: String,
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
style = MaterialTheme.typography.body2,
fontWeight = FontWeight.Bold,
text = title,
)
Text(
style = MaterialTheme.typography.body1,
textAlign = TextAlign.Center,
text = label,
)
}
}
@Composable
private fun HtmlText(
html: String,
modifier: Modifier = Modifier
) {
AndroidView(
modifier = modifier,
factory = { context -> TextView(context) },
update = { it.text = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT) }
)
}
@Composable
@ -25,6 +200,8 @@ private fun DetailScreenContent() {
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
private fun DetailScreenContentPreview() {
BibLibTheme {
DetailScreenContent()
// DetailScreenContent(
//
// )
}
}

View file

@ -11,6 +11,7 @@ import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
@ -93,14 +94,14 @@ private fun Cover(
) {
val cover by image
AnimatedContent(
targetState = cover,
targetState = cover.painter,
transitionSpec = { fadeIn() with fadeOut() }
) {
Image(
modifier = modifier,
alignment = Alignment.Center,
contentScale = it.contentScale,
painter = it.painter,
contentScale = cover.contentScale,
painter = cover.painter,
contentDescription = null,
)
}

View file

@ -28,24 +28,30 @@ import kotlinx.coroutines.flow.flowOf
@Composable
fun LazyBookThumbnailColumn(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(MaterialTheme.bibLib.dimen.medium),
contentPadding: PaddingValues = PaddingValues(all = MaterialTheme.bibLib.dimen.medium),
state: LazyListState = rememberLazyListState(),
books: LazyPagingItems<BookThumbnailUio>,
onItemClick: (BookThumbnailUio) -> Unit = {},
) {
LazyColumn(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(MaterialTheme.bibLib.dimen.small),
contentPadding = PaddingValues(all = MaterialTheme.bibLib.dimen.small),
verticalArrangement = verticalArrangement,
contentPadding = contentPadding,
state = state,
) {
items(books) { thumbnail ->
BookThumbnail(thumbnail)
BookThumbnail(
thumbnail = thumbnail,
onClick = onItemClick,
)
}
}
}
@Composable
@Preview
fun LazyBookThumbnailColumnPreview() {
private fun LazyBookThumbnailColumnPreview() {
BibLibTheme {
LazyBookThumbnailColumn(
modifier = Modifier.fillMaxSize(),
@ -55,7 +61,7 @@ fun LazyBookThumbnailColumnPreview() {
}
@Composable
fun previewResources(): LazyPagingItems<BookThumbnailUio> {
private fun previewResources(): LazyPagingItems<BookThumbnailUio> {
val cover = CoverUio(
painter = painterResource(id = R.drawable.ic_baseline_auto_stories_24),
contentScale = ContentScale.None,

View file

@ -1,19 +1,31 @@
package com.pixelized.biblib.ui.screen.home.page.books
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.biblib.ui.screen.home.page.news.NewsPage
import com.pixelized.biblib.ui.navigation.screen.LocalScreenNavHostController
import com.pixelized.biblib.ui.navigation.screen.navigateToBookDetail
import com.pixelized.biblib.ui.screen.home.common.composable.LazyBookThumbnailColumn
import com.pixelized.biblib.ui.screen.home.page.news.NewsPage
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.utils.extention.bibLib
@Composable
fun BooksPage(
booksViewModel: BooksViewModel = hiltViewModel()
) {
val navHostController = LocalScreenNavHostController.current
LazyBookThumbnailColumn(
books = booksViewModel.books
verticalArrangement = Arrangement.spacedBy(MaterialTheme.bibLib.dimen.thumbnail.arrangement),
contentPadding = PaddingValues(all = MaterialTheme.bibLib.dimen.thumbnail.padding),
books = booksViewModel.books,
onItemClick = {
navHostController.navigateToBookDetail(bookId = it.id)
},
)
}

View file

@ -1,18 +1,30 @@
package com.pixelized.biblib.ui.screen.home.page.news
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.biblib.ui.navigation.screen.LocalScreenNavHostController
import com.pixelized.biblib.ui.navigation.screen.navigateToBookDetail
import com.pixelized.biblib.ui.screen.home.common.composable.LazyBookThumbnailColumn
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.utils.extention.bibLib
@Composable
fun NewsPage(
booksViewModel: NewsBookViewModel = hiltViewModel()
) {
val navHostController = LocalScreenNavHostController.current
LazyBookThumbnailColumn(
books = booksViewModel.news
verticalArrangement = Arrangement.spacedBy(MaterialTheme.bibLib.dimen.thumbnail.arrangement),
contentPadding = PaddingValues(all = MaterialTheme.bibLib.dimen.thumbnail.padding),
books = booksViewModel.news,
onItemClick = {
navHostController.navigateToBookDetail(bookId = it.id)
},
)
}

View file

@ -1,25 +1,123 @@
package com.pixelized.biblib.ui.screen.launch
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.navigation.screen.Screen
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class LauncherViewModel @Inject constructor() : ViewModel() {
class LauncherViewModel @Inject constructor(
private val credentialRepository: ICredentialRepository,
private val googleSignIn: IGoogleSingInRepository,
private val client: IBibLibClient,
private val bookRepository: IBookRepository,
private val apiCache: IAPICacheRepository,
) : ViewModel() {
private val _isLoading = mutableStateOf(true)
val isLoading: State<Boolean> get() = _isLoading
val isLoading: Boolean by _isLoading
var startDestination: Screen = Screen.Authentication
private set
init {
viewModelScope.launch(Dispatchers.IO) {
delay(1500)
// Try to Authenticate
if (autoLoginWithGoogle() || autologinWithCredential()) {
startDestination = Screen.Home
// Update book
if (loadNewBooks()) {
loadAllBooks()
}
}
//
_isLoading.value = false
}
}
//////////////////////////////////////
// region: Authentication
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
}
}
// endregion
//////////////////////////////////////
// region: Books update
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 newIds = apiCache.new?.data?.map { it.book_id } ?: listOf()
val factory = BookFactory()
val books = response.data?.map { dto ->
val isNew = newIds.contains(dto.book_id)
val index = newIds.indexOf(dto.book_id)
factory.fromListResponseToBook(dto, isNew, index)
}
books?.let { data -> bookRepository.update(data) }
}
return true
}
}

View file

@ -17,7 +17,8 @@ data class BibLibDimen(
val extraLarge: Dp = 64.dp,
val dialog: Dialog = Dialog(),
val thumbnail: BookThumbnail = BookThumbnail(),
) {
) {
@Stable
@Immutable
data class Dialog(
@ -29,6 +30,8 @@ data class BibLibDimen(
@Stable
@Immutable
data class BookThumbnail(
val padding: Dp = 16.dp,
val arrangement: Dp = 8.dp,
val cover: DpSize = DpSize(60.dp, 96.dp),
val corner: Dp = 8.dp,
)

View file

@ -8,12 +8,6 @@ import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import com.google.accompanist.drawablepainter.DrawablePainter
@Composable
fun painterResource(@DrawableRes res: Int): Painter {
val context = LocalContext.current
val drawable = AppCompatResources.getDrawable(context, res)
return DrawablePainter(drawable = drawable ?: error(""))
}
fun painterResource(context: Context, @DrawableRes res: Int): Painter {
val drawable = AppCompatResources.getDrawable(context, res)