Composable refactor (prepare for jetpack Navigation)

This commit is contained in:
Thomas Andres Gomez 2021-05-19 11:31:44 +02:00
parent 5053ac7360
commit 0ce392bba7
17 changed files with 597 additions and 542 deletions

View file

@ -9,9 +9,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.lifecycle.viewmodel.compose.viewModel
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.composable.screen.LoginScreen
import com.pixelized.biblib.ui.composable.screen.HomeScreen
import com.pixelized.biblib.ui.composable.screen.SplashScreen
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel.Navigable.Screen
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
@ -43,9 +43,9 @@ fun ContentComposable() {
Crossfade(targetState = main) {
when (it) {
is Screen.SplashScreen -> SplashScreenComposable()
is Screen.LoginScreen -> LoginScreenComposable()
is Screen.MainScreen -> MainScreenComposable()
is Screen.SplashScreen -> SplashScreen()
is Screen.LoginScreen -> LoginScreen()
is Screen.MainScreen -> HomeScreen()
}
}
}

View file

@ -0,0 +1,131 @@
package com.pixelized.biblib.ui.composable.common
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.pixelized.biblib.BuildConfig
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.theme.BibLibTheme
import java.util.*
@Composable
fun BibLibDrawer(
onNewClick: () -> Unit = {},
onBookClick: () -> Unit = {},
onSeriesClick: () -> Unit = {},
onAuthorClick: () -> Unit = {},
onTagClick: () -> Unit = {},
) {
val typography = MaterialTheme.typography
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
Card(
shape = CutCornerShape(bottomEnd = 16.dp),
elevation = 8.dp
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
style = typography.body1,
text = "R. Daneel Olivaw"
)
Text(
style = typography.caption,
text = "r.daneel.olivaw@biblib.com"
)
}
}
Spacer(modifier = Modifier.height(8.dp))
DrawerItem(
text = "Nouveautés",
imageVector = Icons.Default.NewReleases,
onClick = onNewClick,
)
DrawerItem(
text = "Livres",
imageVector = Icons.Default.AutoStories,
onClick = onBookClick,
)
DrawerItem(
text = "Séries",
imageVector = Icons.Default.AutoAwesomeMotion,
onClick = onSeriesClick,
)
DrawerItem(
text = "Auteurs",
imageVector = Icons.Default.SupervisorAccount,
onClick = onAuthorClick,
)
DrawerItem(
text = "Étiquettes",
imageVector = Icons.Default.LocalOffer,
onClick = onTagClick,
)
Spacer(modifier = Modifier.weight(1f))
Text(
modifier = Modifier
.align(Alignment.End)
.padding(16.dp),
style = typography.caption,
text = stringResource(
R.string.app_version,
BuildConfig.BUILD_TYPE.toUpperCase(Locale.getDefault()),
BuildConfig.VERSION_NAME,
BuildConfig.VERSION_CODE
)
)
}
}
@Composable
private fun DrawerItem(text: String, imageVector: ImageVector, onClick: () -> Unit) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.height(52.dp)
.clickable { onClick() }) {
Icon(
modifier = Modifier.padding(horizontal = 16.dp),
imageVector = imageVector,
contentDescription = ""
)
Text(
text = text,
)
Spacer(modifier = Modifier.weight(1f))
Icon(
modifier = Modifier.padding(end = 8.dp),
imageVector = Icons.Default.NavigateNext,
contentDescription = ""
)
}
}
@Preview
@Composable
fun BibLibDrawerLightPreview() {
BibLibTheme {
BibLibDrawer()
}
}

View file

@ -0,0 +1,77 @@
package com.pixelized.biblib.ui.composable.common
import androidx.compose.animation.Crossfade
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.sharp.ArrowBack
import androidx.compose.material.icons.sharp.Menu
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.theme.BibLibTheme
@Composable
fun BibLibToolbar(
canNavigateBack: Boolean,
onBackPress: () -> Unit,
onDrawerPress: () -> Unit,
) {
TopAppBar(
title = {
Text(stringResource(id = R.string.app_name))
},
navigationIcon = {
NavigationIcon(
canNavigateBack,
onBackPress,
onDrawerPress,
)
}
)
}
@Composable
private fun NavigationIcon(
canNavigateBack: Boolean,
onBackPress: () -> Unit,
onDrawerPress: () -> Unit,
) {
Crossfade(targetState = canNavigateBack) {
if (it) {
IconButton(onClick = onBackPress) {
Icon(
imageVector = Icons.Sharp.ArrowBack,
contentDescription = "back"
)
}
} else {
IconButton(onClick = onDrawerPress) {
Icon(
imageVector = Icons.Sharp.Menu,
contentDescription = "drawer"
)
}
}
}
}
@Preview
@Composable
fun BibLibToolbarDarkPreview() {
BibLibTheme(darkTheme = false) {
BibLibToolbar(false, {}, {})
}
}
@Preview
@Composable
fun BibLibToolbarLightPreview() {
BibLibTheme(darkTheme = true) {
BibLibToolbar(true, {}, {})
}
}

View file

@ -1,7 +1,6 @@
package com.pixelized.biblib.ui.composable.items.dialog
package com.pixelized.biblib.ui.composable.dialog
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.composable.items.dialog
package com.pixelized.biblib.ui.composable.dialog
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.height

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.composable.items.dialog
package com.pixelized.biblib.ui.composable.dialog
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.ui.composable.items.dialog
package com.pixelized.biblib.ui.composable.dialog
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.height

View file

@ -19,6 +19,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.data.BookThumbnailUio
@ -28,47 +29,31 @@ import com.pixelized.biblib.utils.BitmapCache
import com.pixelized.biblib.utils.injection.Bob
import com.pixelized.biblib.utils.mock.BookThumbnailMock
@Preview
@Composable
fun BookThumbnailComposablePreview() {
Bob[BitmapCache::class] = BitmapCache(LocalContext.current)
BibLibTheme {
val mock = BookThumbnailMock()
BookThumbnailComposable(thumbnail = mock.bookThumbnail)
}
}
@Preview
@Composable
fun BookThumbnailComposablePreviewDark() {
Bob[BitmapCache::class] = BitmapCache(LocalContext.current)
BibLibTheme(darkTheme = true) {
val mock = BookThumbnailMock()
BookThumbnailComposable(thumbnail = mock.bookThumbnail)
}
}
private val THUMBNAIL_WIDTH: Dp = 60.dp
private val THUMBNAIL_HEIGHT: Dp = 96.dp
@Composable
fun BookThumbnailComposable(
fun BookItem(
thumbnail: BookThumbnailUio,
modifier: Modifier = Modifier,
onClick: ((BookThumbnailUio) -> Unit)? = null,
) {
val typography = MaterialTheme.typography
Card(
modifier = modifier.clickable {
onClick?.invoke(thumbnail)
},
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.clickable {
onClick?.invoke(thumbnail)
},
elevation = 4.dp,
) {
Row(
modifier = Modifier.height(96.dp),
modifier = Modifier.height(THUMBNAIL_HEIGHT),
) {
Image(
contentModifier = Modifier
.width(60.dp)
.fillMaxHeight()
.size(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)
.clip(RoundedCornerShape(4.dp)),
placeHolder = painterResource(id = R.drawable.ic_launcher_foreground),
contentScale = ContentScale.FillBounds,
@ -77,7 +62,9 @@ fun BookThumbnailComposable(
contentDescription = thumbnail.title
)
Column(
modifier = Modifier.padding(8.dp).weight(1f)
modifier = Modifier
.padding(8.dp)
.weight(1f)
) {
Text(
style = typography.h6,
@ -91,17 +78,19 @@ fun BookThumbnailComposable(
overflow = TextOverflow.Ellipsis,
softWrap = false,
)
Row(modifier = Modifier.weight(1f), verticalAlignment = Alignment.Bottom) {
Row(
modifier = Modifier.weight(1f),
verticalAlignment = Alignment.Bottom
) {
Text(
style = typography.caption,
text = thumbnail.genre,
overflow = TextOverflow.Ellipsis,
softWrap = false,
)
Spacer(modifier = Modifier.width(4.dp))
Spacer(
modifier = Modifier
.width(0.dp)
.widthIn(min = 4.dp)
.weight(1f)
)
Text(
@ -114,8 +103,28 @@ fun BookThumbnailComposable(
}
Icon(
modifier = Modifier.align(Alignment.CenterVertically),
imageVector = Icons.Default.NavigateNext, contentDescription = "navigate"
imageVector = Icons.Default.NavigateNext, contentDescription = ""
)
}
}
}
@Preview
@Composable
fun BookItemLightPreview() {
Bob[BitmapCache::class] = BitmapCache(LocalContext.current)
BibLibTheme {
val mock = BookThumbnailMock()
BookItem(thumbnail = mock.bookThumbnail)
}
}
@Preview
@Composable
fun BookItemDarkPreview() {
Bob[BitmapCache::class] = BitmapCache(LocalContext.current)
BibLibTheme(darkTheme = true) {
val mock = BookThumbnailMock()
BookItem(thumbnail = mock.bookThumbnail)
}
}

View file

@ -5,10 +5,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Send
@ -19,6 +16,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
@ -40,22 +38,25 @@ import com.pixelized.biblib.utils.mock.BookMock
@Composable
fun DetailPageComposable(
fun DetailPage(
booksViewModel: IBooksViewModel = viewModel<BooksViewModel>(),
bookId: Int
) {
Box(
Modifier.fillMaxWidth().fillMaxHeight()
Surface(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
elevation = 4.dp
) {
val book by booksViewModel.getBookDetail(bookId).observeAsState()
book?.let {
DetailPageComposable(book = it)
DetailPage(book = it)
}
}
}
@Composable
fun DetailPageComposable(book: BookUio) {
fun DetailPage(book: BookUio) {
val typography = MaterialTheme.typography
Column(
@ -81,32 +82,20 @@ fun DetailPageComposable(book: BookUio) {
)
Row(modifier = Modifier.padding(bottom = 16.dp)) {
Button(
modifier = Modifier
.weight(1f)
.padding(end = 4.dp),
onClick = { }) {
Icon(imageVector = Icons.Default.Download, contentDescription = "")
Spacer(modifier = Modifier.width(4.dp))
Text(text = stringResource(id = R.string.action_epub))
}
modifier = Modifier.padding(end = 4.dp),
imageVector = Icons.Default.Download,
text = stringResource(id = R.string.action_epub)
) { }
Button(
modifier = Modifier
.weight(1f)
.padding(horizontal = 4.dp),
onClick = { }) {
Icon(imageVector = Icons.Default.Download, contentDescription = "")
Spacer(modifier = Modifier.width(4.dp))
Text(text = stringResource(id = R.string.action_mobi))
}
modifier = Modifier.padding(horizontal = 4.dp),
imageVector = Icons.Default.Download,
text = stringResource(id = R.string.action_mobi),
) { }
Button(
modifier = Modifier
.weight(1f)
.padding(start = 4.dp),
onClick = { }) {
Icon(imageVector = Icons.Default.Send, contentDescription = "")
Spacer(modifier = Modifier.width(4.dp))
Text(text = stringResource(id = R.string.action_send))
}
modifier = Modifier.padding(start = 4.dp),
imageVector = Icons.Default.Send,
text = stringResource(id = R.string.action_send),
) { }
}
Text(
modifier = Modifier
@ -124,62 +113,29 @@ fun DetailPageComposable(book: BookUio) {
text = book.author,
)
Row(modifier = Modifier.padding(bottom = 8.dp)) {
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
style = typography.body2,
fontWeight = FontWeight.Bold,
text = stringResource(id = R.string.detail_rating),
)
Text(
style = typography.body1,
text = book.rating.toString(),
)
}
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
style = typography.body2,
fontWeight = FontWeight.Bold,
text = stringResource(id = R.string.detail_language),
)
Text(
style = typography.body1,
text = book.language,
)
}
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
style = typography.body2,
fontWeight = FontWeight.Bold,
text = stringResource(id = R.string.detail_release),
)
Text(
style = typography.body1,
text = book.date,
)
}
TitleLabel(
title = stringResource(id = R.string.detail_rating),
label = book.rating.toString(),
)
TitleLabel(
title = stringResource(id = R.string.detail_language),
label = book.language,
)
TitleLabel(
title = stringResource(id = R.string.detail_release),
label = book.date,
)
}
Row(modifier = Modifier.padding(bottom = 16.dp)) {
TitleLabel(
title = stringResource(id = R.string.detail_genre),
label = book.genre,
)
TitleLabel(
title = stringResource(id = R.string.detail_series),
label = book.series ?: "-",
)
}
Text(
modifier = Modifier.align(Alignment.CenterHorizontally),
style = typography.body2,
fontWeight = FontWeight.Bold,
text = stringResource(id = R.string.detail_series),
)
Text(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(bottom = 16.dp),
style = typography.body1,
text = book.series ?: "-",
)
Text(
style = typography.body1,
text = book.description,
@ -188,12 +144,51 @@ fun DetailPageComposable(book: BookUio) {
}
}
@Composable
private fun RowScope.TitleLabel(
title: String,
label: String,
) {
val typography = MaterialTheme.typography
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
style = typography.body2,
fontWeight = FontWeight.Bold,
text = title,
)
Text(
style = typography.body1,
text = label,
)
}
}
@Composable
private fun RowScope.Button(
modifier: Modifier = Modifier,
imageVector: ImageVector,
text: String,
onClick: () -> Unit,
) {
Button(
modifier = modifier.weight(1f),
onClick = onClick
) {
Icon(imageVector = imageVector, contentDescription = "")
Spacer(modifier = Modifier.width(4.dp))
Text(text = text)
}
}
@Preview
@Composable
fun DetailPageComposablePreview() {
fun DetailPageLightPreview() {
Bob[BitmapCache::class] = BitmapCache(LocalContext.current)
BibLibTheme {
val mock = BookMock()
DetailPageComposable(mock.book)
DetailPage(mock.book)
}
}

View file

@ -4,8 +4,6 @@ import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
@ -15,7 +13,7 @@ import androidx.compose.ui.unit.dp
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items
import com.pixelized.biblib.ui.composable.items.BookThumbnailComposable
import com.pixelized.biblib.ui.composable.items.BookItem
import com.pixelized.biblib.ui.data.BookThumbnailUio
import com.pixelized.biblib.ui.viewmodel.book.IBooksViewModel
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel
@ -23,7 +21,7 @@ import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel.Navigab
@Composable
fun HomePageComposable(
fun HomePage(
navigationViewModel: INavigationViewModel,
booksViewModel: IBooksViewModel,
page: Page?,
@ -32,7 +30,7 @@ fun HomePageComposable(
currentPage = page as? Page.Home ?: currentPage
// https://issuetracker.google.com/issues/177245496
val data: LazyPagingItems<BookThumbnailUio> = when(currentPage) {
val data: LazyPagingItems<BookThumbnailUio> = when (currentPage) {
is Page.Home.New -> booksViewModel.books.collectAsLazyPagingItems()
else -> booksViewModel.news.collectAsLazyPagingItems()
}
@ -47,12 +45,7 @@ fun HomePageComposable(
state = lazyListState,
) {
items(data) { thumbnail ->
BookThumbnailComposable(
thumbnail = thumbnail!!,
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
) { item ->
BookItem(thumbnail = thumbnail!!) { item ->
navigationViewModel.navigateTo(Page.Detail(item.id))
}
}

View file

@ -0,0 +1,115 @@
package com.pixelized.biblib.ui.composable.screen
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.material.Scaffold
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.biblib.ui.composable.common.BibLibDrawer
import com.pixelized.biblib.ui.composable.common.BibLibToolbar
import com.pixelized.biblib.ui.composable.pages.DetailPage
import com.pixelized.biblib.ui.composable.pages.HomePage
import com.pixelized.biblib.ui.theme.Animation
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.viewmodel.book.BooksViewModel
import com.pixelized.biblib.ui.viewmodel.book.IBooksViewModel
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel.Navigable.Page
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
import kotlinx.coroutines.launch
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun HomeScreen(
navigationViewModel: INavigationViewModel = viewModel<NavigationViewModel>(),
booksViewModel: IBooksViewModel = viewModel<BooksViewModel>(),
) {
// navigation
val page by navigationViewModel.page.observeAsState()
// coroutine
val coroutineScope = rememberCoroutineScope()
// scaffold & toolbar
val canNavigateBack = page !is Page.Home
val scaffoldState = rememberScaffoldState()
// TODO:
LaunchedEffect(key1 = "MainScreen", block = {
navigationViewModel.navigateTo(Page.Home.New, true)
})
Scaffold(
scaffoldState = scaffoldState,
topBar = {
BibLibToolbar(
canNavigateBack,
onBackPress = {
navigationViewModel.navigateBack()
},
onDrawerPress = {
coroutineScope.launch { scaffoldState.drawerState.open() }
}
)
},
drawerContent = {
BibLibDrawer(
onNewClick = {
coroutineScope.launch { scaffoldState.drawerState.close() }
},
onBookClick = {
coroutineScope.launch { scaffoldState.drawerState.close() }
},
onSeriesClick = {
coroutineScope.launch { scaffoldState.drawerState.close() }
},
onAuthorClick = {
coroutineScope.launch { scaffoldState.drawerState.close() }
},
onTagClick = {
coroutineScope.launch { scaffoldState.drawerState.close() }
},
)
}
) {
HomePage(
navigationViewModel = navigationViewModel,
booksViewModel = booksViewModel,
page = page,
)
AnimatedVisibility(
visible = page is Page.Detail,
initiallyVisible = false,
enter = slideInVertically(
animationSpec = tween(Animation.MEDIUM_DURATION),
initialOffsetY = { height -> height },
),
exit = slideOutVertically(
animationSpec = tween(Animation.MEDIUM_DURATION),
targetOffsetY = { height -> height },
),
) {
// Small trick to display the detail page during animation exit.
var currentPage by remember { mutableStateOf<Page.Detail?>(null) }
currentPage = page as? Page.Detail ?: currentPage
currentPage?.let { DetailPage(booksViewModel, it.bookId) }
}
}
}
@Preview
@Composable
fun MainScreenComposablePreview() {
BibLibTheme {
HomeScreen(
INavigationViewModel.Mock(page = Page.Home.New),
IBooksViewModel.Mock()
)
}
}

View file

@ -38,10 +38,10 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.biblib.R
import com.pixelized.biblib.network.client.IBibLibClient.Companion.REGISTER_URL
import com.pixelized.biblib.ui.composable.items.dialog.CrossFadeOverlay
import com.pixelized.biblib.ui.composable.items.dialog.ErrorCard
import com.pixelized.biblib.ui.composable.items.dialog.LoadingCard
import com.pixelized.biblib.ui.composable.items.dialog.SuccessCard
import com.pixelized.biblib.ui.composable.dialog.CrossFadeOverlay
import com.pixelized.biblib.ui.composable.dialog.ErrorCard
import com.pixelized.biblib.ui.composable.dialog.LoadingCard
import com.pixelized.biblib.ui.composable.dialog.SuccessCard
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.viewmodel.authentication.AuthenticationViewModel
import com.pixelized.biblib.ui.viewmodel.authentication.IAuthenticationViewModel
@ -58,7 +58,7 @@ import kotlinx.coroutines.delay
private const val LE_LOAD_BOOK = "LE_LOAD_BOOK"
@Composable
fun LoginScreenComposable(
fun LoginScreen(
navigationViewModel: INavigationViewModel = viewModel<NavigationViewModel>(),
credentialViewModel: ICredentialViewModel = viewModel<CredentialViewModel>(),
authenticationViewModel: IAuthenticationViewModel = viewModel<AuthenticationViewModel>(),
@ -76,13 +76,13 @@ fun LoginScreenComposable(
authenticationViewModel,
bookViewModel
)
LoginScreenContentComposable(credentialViewModel, authenticationViewModel)
LoginScreenDialogComposable(authenticationViewModel, bookViewModel)
Content(credentialViewModel, authenticationViewModel)
Dialogs(authenticationViewModel, bookViewModel)
}
}
@Composable
private fun LoginScreenContentComposable(
private fun Content(
credentialViewModel: ICredentialViewModel,
authenticationViewModel: IAuthenticationViewModel,
) {
@ -103,7 +103,7 @@ private fun LoginScreenContentComposable(
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun LoginScreenDialogComposable(
private fun Dialogs(
authenticationViewModel: IAuthenticationViewModel,
booksViewModel: IBooksViewModel,
) {
@ -383,7 +383,7 @@ private fun CredentialRemember(
@Composable
fun LoginScreenComposablePreview() {
BibLibTheme {
LoginScreenComposable(
LoginScreen(
navigationViewModel = INavigationViewModel.Mock(),
credentialViewModel = ICredentialViewModel.Mock(),
authenticationViewModel = IAuthenticationViewModel.Mock(),

View file

@ -1,285 +0,0 @@
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.foundation.shape.CutCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.sharp.ArrowBack
import androidx.compose.material.icons.sharp.Menu
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
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.pages.DetailPageComposable
import com.pixelized.biblib.ui.composable.pages.HomePageComposable
import com.pixelized.biblib.ui.theme.Animation
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.viewmodel.book.BooksViewModel
import com.pixelized.biblib.ui.viewmodel.book.IBooksViewModel
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel.Navigable.Page
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
import kotlinx.coroutines.launch
import java.util.*
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun MainScreenComposable(
navigationViewModel: INavigationViewModel = viewModel<NavigationViewModel>(),
booksViewModel: IBooksViewModel = viewModel<BooksViewModel>(),
) {
// navigation
val page by navigationViewModel.page.observeAsState()
// coroutine
val coroutineScope = rememberCoroutineScope()
// scaffold & toolbar
val canNavigateBack = page !is Page.Home
val scaffoldState = rememberScaffoldState()
// TODO:
LaunchedEffect(key1 = "MainScreen", block = {
navigationViewModel.navigateTo(Page.Home.New, true)
})
Scaffold(
scaffoldState = scaffoldState,
topBar = {
ToolbarComposable(
canNavigateBack,
onBackPress = {
navigationViewModel.navigateBack()
},
onDrawerPress = {
coroutineScope.launch { scaffoldState.drawerState.open() }
}
)
},
drawerContent = {
DrawerContentComposable(
onNewClick = {
coroutineScope.launch { scaffoldState.drawerState.close() }
},
onBookClick = {
coroutineScope.launch { scaffoldState.drawerState.close() }
},
onSeriesClick = {
coroutineScope.launch { scaffoldState.drawerState.close() }
},
onAuthorClick = {
coroutineScope.launch { scaffoldState.drawerState.close() }
},
onTagClick = {
coroutineScope.launch { scaffoldState.drawerState.close() }
},
)
}
) {
HomePageComposable(
navigationViewModel = navigationViewModel,
booksViewModel = booksViewModel,
page = page,
)
AnimatedVisibility(
visible = page is Page.Detail,
initiallyVisible = false,
enter = slideInVertically(
animationSpec = tween(Animation.MEDIUM_DURATION),
initialOffsetY = { height -> height },
),
exit = slideOutVertically(
animationSpec = tween(Animation.MEDIUM_DURATION),
targetOffsetY = { height -> height },
),
) {
// Small trick to display the detail page during animation exit.
var currentPage by remember { mutableStateOf<Page.Detail?>(null) }
currentPage = page as? Page.Detail ?: currentPage
currentPage?.let { DetailPageComposable(booksViewModel, it.bookId) }
}
}
}
@Composable
fun ToolbarComposable(
canNavigateBack: Boolean,
onBackPress: () -> Unit,
onDrawerPress: () -> Unit,
) {
TopAppBar(
title = {
Text(stringResource(id = R.string.app_name))
},
navigationIcon = {
NavigationIcon(
canNavigateBack,
onBackPress,
onDrawerPress,
)
}
)
}
@Composable
fun NavigationIcon(
canNavigateBack: Boolean,
onBackPress: () -> Unit,
onDrawerPress: () -> Unit,
) {
Crossfade(targetState = canNavigateBack) {
if (it) {
IconButton(onClick = onBackPress) {
Icon(
imageVector = Icons.Sharp.ArrowBack,
contentDescription = "back"
)
}
} else {
IconButton(onClick = onDrawerPress) {
Icon(
imageVector = Icons.Sharp.Menu,
contentDescription = "drawer"
)
}
}
}
}
@Composable
fun DrawerContentComposable(
onNewClick: () -> Unit,
onBookClick: () -> Unit,
onSeriesClick: () -> Unit,
onAuthorClick: () -> Unit,
onTagClick: () -> Unit,
) {
val typography = MaterialTheme.typography
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
Card(
shape = CutCornerShape(bottomEnd = 16.dp),
elevation = 8.dp
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
style = typography.body1,
text = "R. Daneel Olivaw"
)
Text(
style = typography.caption,
text = "r.daneel.olivaw@biblib.com"
)
}
}
Spacer(modifier = Modifier.height(8.dp))
DrawerLink(
text = "Nouveautés",
imageVector = Icons.Default.NewReleases,
onClick = onNewClick,
)
DrawerLink(
text = "Livres",
imageVector = Icons.Default.AutoStories,
onClick = onBookClick,
)
DrawerLink(
text = "Séries",
imageVector = Icons.Default.AutoAwesomeMotion,
onClick = onSeriesClick,
)
DrawerLink(
text = "Auteurs",
imageVector = Icons.Default.SupervisorAccount,
onClick = onAuthorClick,
)
DrawerLink(
text = "Étiquettes",
imageVector = Icons.Default.LocalOffer,
onClick = onTagClick,
)
Spacer(modifier = Modifier.weight(1f))
Text(
modifier = Modifier
.align(Alignment.End)
.padding(16.dp),
style = typography.caption,
text = stringResource(
R.string.app_version,
BuildConfig.BUILD_TYPE.toUpperCase(Locale.getDefault()),
BuildConfig.VERSION_NAME,
BuildConfig.VERSION_CODE
)
)
}
}
@Composable
fun DrawerLink(text: String, imageVector: ImageVector, onClick: () -> Unit) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.height(52.dp)
.clickable { onClick() }) {
Icon(
modifier = Modifier.padding(horizontal = 16.dp),
imageVector = imageVector,
contentDescription = ""
)
Text(
text = text,
)
Spacer(modifier = Modifier.weight(1f))
Icon(
modifier = Modifier.padding(end = 8.dp),
imageVector = Icons.Default.NavigateNext,
contentDescription = ""
)
}
}
@Preview
@Composable
fun ToolbarComposableDarkPreview() {
BibLibTheme(darkTheme = false) {
ToolbarComposable(false, {}, {})
}
}
@Preview
@Composable
fun ToolbarComposableLightPreview() {
BibLibTheme(darkTheme = true) {
ToolbarComposable(true, {}, {})
}
}
@Preview
@Composable
fun MainScreenComposablePreview() {
BibLibTheme {
MainScreenComposable(
INavigationViewModel.Mock(page = Page.Home.New),
IBooksViewModel.Mock()
)
}
}

View file

@ -5,6 +5,7 @@ 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.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
@ -16,8 +17,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.composable.dialog.CrossFadeOverlay
import com.pixelized.biblib.ui.composable.dialog.ErrorCard
import com.pixelized.biblib.ui.theme.Animation
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.viewmodel.authentication.AuthenticationViewModel
@ -30,29 +31,14 @@ import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
import kotlinx.coroutines.delay
import java.util.*
private const val LAUNCH_EFFECT_AUTHENTICATION = "LAUNCH_EFFECT_AUTHENTICATION"
private const val LAUNCH_EFFECT_ENTER = "LAUNCH_EFFECT_ENTER"
private const val LAUNCH_EFFECT_EXIT = "LAUNCH_EFFECT_EXIT"
private const val LAUNCH_EFFECT_BOOK = "LAUNCH_EFFECT_BOOK"
private const val LAUNCH_EFFECT_NAVIGATION = "LAUNCH_EFFECT_NAVIGATION"
@Preview
@Composable
fun SplashScreenComposablePreview() {
BibLibTheme {
val authenticationViewModel = IAuthenticationViewModel.Mock()
val bookViewModel = IBooksViewModel.Mock()
val navigation = INavigationViewModel.Mock()
SplashScreenComposable(
authenticationViewModel = authenticationViewModel,
bookViewModel = bookViewModel,
navigationViewModel = navigation,
initiallyVisible = true
)
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun SplashScreenComposable(
fun SplashScreen(
authenticationViewModel: IAuthenticationViewModel = viewModel<AuthenticationViewModel>(),
bookViewModel: IBooksViewModel = viewModel<BooksViewModel>(),
navigationViewModel: INavigationViewModel = viewModel<NavigationViewModel>(),
@ -60,23 +46,24 @@ fun SplashScreenComposable(
) {
val authenticationState: IAuthenticationViewModel.State? by authenticationViewModel.state.observeAsState()
val bookState by bookViewModel.state.observeAsState()
val contentVisible = remember { mutableStateOf(false) }
val contentVisibility = remember { mutableStateOf(false) }
// Content
ContentComposable(
visible = contentVisible.value,
Content(
visible = contentVisibility.value,
initiallyVisible = initiallyVisible
)
// Dialog
AuthenticationError(state = authenticationState as? IAuthenticationViewModel.State.Error)
BookError(state = bookState as? IBooksViewModel.State.Error)
AuthenticationError(
state = authenticationState as? IAuthenticationViewModel.State.Error
)
BookError(
state = bookState as? IBooksViewModel.State.Error
)
LaunchedEffect(LAUNCH_EFFECT_AUTHENTICATION) {
delay(Animation.SHORT_DURATION.toLong())
contentVisible.value = true
delay(Animation.LONG_DURATION.toLong())
delay(Animation.SHORT_DURATION.toLong())
HandleEnterAnimation(contentVisibility) {
authenticationViewModel.autoLogin()
}
@ -86,16 +73,12 @@ fun SplashScreenComposable(
bookViewModel.updateBooks()
}
(bookState as? IBooksViewModel.State.Finished)?.let {
LaunchedEffect(LAUNCH_EFFECT_NAVIGATION) {
contentVisible.value = false
delay(Animation.LONG_DURATION.toLong())
HandleExitAnimation(contentVisibility) {
navigationViewModel.navigateTo(Screen.MainScreen)
}
}
} else {
LaunchedEffect(LAUNCH_EFFECT_NAVIGATION) {
contentVisible.value = false
delay(Animation.LONG_DURATION.toLong())
HandleExitAnimation(contentVisibility) {
navigationViewModel.navigateTo(Screen.LoginScreen)
}
}
@ -104,81 +87,83 @@ fun SplashScreenComposable(
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun ContentComposable(
private fun Content(
duration: Int = Animation.LONG_DURATION,
visible: Boolean = false,
initiallyVisible: Boolean = false,
) {
val typography = MaterialTheme.typography
Box(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth()
.padding(16.dp),
) {
Column(
Surface {
Box(
modifier = Modifier
.width(240.dp)
.align(Alignment.Center)
.fillMaxHeight()
.fillMaxWidth()
.padding(16.dp),
) {
AnimatedVisibility(
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)
),
Column(
modifier = Modifier
.width(240.dp)
.align(Alignment.Center)
) {
Text(
style = typography.h4,
text = stringResource(id = R.string.splash_welcome)
)
AnimatedVisibility(
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.splash_welcome)
)
}
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)
)
}
}
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)
)
}
}
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
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
)
)
)
}
}
}
}
@ -225,4 +210,39 @@ private fun BookError(state: IBooksViewModel.State.Error?) {
)
}
}
}
@Composable
fun HandleEnterAnimation(contentVisibility: MutableState<Boolean>, then: suspend () -> Unit) {
LaunchedEffect(LAUNCH_EFFECT_ENTER) {
delay(Animation.SHORT_DURATION.toLong())
contentVisibility.value = true
delay(Animation.LONG_DURATION.toLong() + Animation.SHORT_DURATION.toLong())
then()
}
}
@Composable
fun HandleExitAnimation(contentVisibility: MutableState<Boolean>, then: suspend () -> Unit) {
LaunchedEffect(LAUNCH_EFFECT_EXIT) {
contentVisibility.value = false
delay(Animation.LONG_DURATION.toLong())
then()
}
}
@Preview
@Composable
fun SplashScreenPreview() {
BibLibTheme(darkTheme = true) {
val authenticationViewModel = IAuthenticationViewModel.Mock()
val bookViewModel = IBooksViewModel.Mock()
val navigation = INavigationViewModel.Mock()
SplashScreen(
authenticationViewModel = authenticationViewModel,
bookViewModel = bookViewModel,
navigationViewModel = navigation,
initiallyVisible = true
)
}
}

View file

@ -2,7 +2,7 @@ package com.pixelized.biblib.ui.theme
object Animation {
const val SHORT_DURATION = 300
const val MEDIUM_DURATION = 500
const val LONG_DURATION = 1000
const val SHORT_DURATION = 250
const val MEDIUM_DURATION = SHORT_DURATION * 2
const val LONG_DURATION = MEDIUM_DURATION * 2
}

View file

@ -35,5 +35,6 @@
<string name="detail_rating">Rating</string>
<string name="detail_language">Language</string>
<string name="detail_release">Release</string>
<string name="detail_genre">Genre</string>
<string name="detail_series">Series</string>
</resources>