Refactor screen/page navigation + thumbnail list.
This commit is contained in:
parent
5a1ff33a46
commit
0ee7de3cde
44 changed files with 1050 additions and 158 deletions
|
|
@ -77,8 +77,6 @@ android {
|
|||
excludes += '/META-INF/{AL2.0,LGPL2.1}'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
@ -109,6 +107,7 @@ dependencies {
|
|||
// Accompanist
|
||||
implementation "com.google.accompanist:accompanist-systemuicontroller:0.24.3-alpha"
|
||||
implementation "com.google.accompanist:accompanist-insets:0.24.3-alpha"
|
||||
implementation "com.google.accompanist:accompanist-drawablepainter:0.24.3-alpha"
|
||||
|
||||
// Navigation
|
||||
implementation "androidx.navigation:navigation-compose:2.4.2"
|
||||
|
|
|
|||
|
|
@ -4,15 +4,19 @@ import android.os.Bundle
|
|||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
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.pixelized.biblib.ui.launch.LauncherViewModel
|
||||
import com.pixelized.biblib.ui.navigation.FullScreenNavHost
|
||||
import com.pixelized.biblib.ui.navigation.Screen
|
||||
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.theme.BibLibTheme
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
|
|
@ -40,9 +44,17 @@ class MainActivity : ComponentActivity() {
|
|||
BibLibTheme {
|
||||
ProvideWindowInsets {
|
||||
Surface(color = MaterialTheme.colors.background) {
|
||||
// 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()) {
|
||||
FullScreenNavHost(
|
||||
ScreenNavHost(
|
||||
startDestination = Screen.Authentication
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
package com.pixelized.biblib.ui.composable
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
fun AnimatedDelayer(
|
||||
content: @Composable AnimatedDelayerScope.() -> Unit
|
||||
) {
|
||||
val scope = AnimatedDelayerScope()
|
||||
scope.content()
|
||||
}
|
||||
|
||||
class AnimatedDelayerScope(
|
||||
var delay: Delay = Delay()
|
||||
)
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.pixelized.biblib.ui.composable.animation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
fun AnimatedDelayer(
|
||||
content: @Composable AnimatedDelayerScope.() -> Unit
|
||||
) {
|
||||
val scope = AnimatedDelayerScope()
|
||||
scope.content()
|
||||
}
|
||||
|
||||
class AnimatedDelayerScope(
|
||||
var delay: Delay = Delay()
|
||||
)
|
||||
|
||||
@JvmInline
|
||||
value class Delay(
|
||||
val value: Int = DELTA,
|
||||
) {
|
||||
operator fun inc(): Delay {
|
||||
return Delay(value = value + DELTA)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DELTA = 100
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.biblib.ui.composable
|
||||
package com.pixelized.biblib.ui.composable.animation
|
||||
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
|
@ -115,19 +115,6 @@ private class TransitionData(
|
|||
val offset by offset
|
||||
}
|
||||
|
||||
@JvmInline
|
||||
value class Delay(
|
||||
val value: Int = DELTA,
|
||||
) {
|
||||
operator fun inc(): Delay {
|
||||
return Delay(value = value + DELTA)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DELTA = 100
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private inline fun <reified T> rememberSavableMutableTransitionState(
|
||||
initialState: T, targetState: T
|
||||
|
|
@ -35,8 +35,8 @@ fun ErrorCard(
|
|||
modifier = Modifier
|
||||
.padding(MaterialTheme.bibLib.dimen.medium)
|
||||
.sizeIn(
|
||||
minWidth = MaterialTheme.bibLib.dimen.dialog.minWidth,
|
||||
minHeight = MaterialTheme.bibLib.dimen.dialog.minHeight,
|
||||
minWidth = MaterialTheme.bibLib.dimen.dialog.minimum.width,
|
||||
minHeight = MaterialTheme.bibLib.dimen.dialog.minimum.height,
|
||||
),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
|
|
|
|||
|
|
@ -31,8 +31,8 @@ fun LoadingCard(
|
|||
modifier = Modifier
|
||||
.padding(MaterialTheme.bibLib.dimen.medium)
|
||||
.sizeIn(
|
||||
minWidth = MaterialTheme.bibLib.dimen.dialog.minWidth,
|
||||
minHeight = MaterialTheme.bibLib.dimen.dialog.minHeight,
|
||||
minWidth = MaterialTheme.bibLib.dimen.dialog.minimum.width,
|
||||
minHeight = MaterialTheme.bibLib.dimen.dialog.minimum.height,
|
||||
),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ fun SuccessCard(
|
|||
modifier = Modifier
|
||||
.padding(MaterialTheme.bibLib.dimen.medium)
|
||||
.sizeIn(
|
||||
minWidth = MaterialTheme.bibLib.dimen.dialog.minWidth,
|
||||
minHeight = MaterialTheme.bibLib.dimen.dialog.minHeight,
|
||||
minWidth = MaterialTheme.bibLib.dimen.dialog.minimum.width,
|
||||
minHeight = MaterialTheme.bibLib.dimen.dialog.minimum.height,
|
||||
),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
package com.pixelized.biblib.ui.navigation
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.google.accompanist.systemuicontroller.SystemUiController
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import com.pixelized.biblib.ui.authentication.AuthenticationScreen
|
||||
import com.pixelized.biblib.ui.composable.SystemThemeColor
|
||||
|
||||
val LocalFullScreenNavHostController = compositionLocalOf<NavHostController> {
|
||||
error("LocalFullScreenNavHostController is not ready yet.")
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FullScreenNavHost(
|
||||
navHostController: NavHostController = rememberNavController(),
|
||||
startDestination: Screen = Screen.Authentication
|
||||
) {
|
||||
val systemUiController: SystemUiController = rememberSystemUiController()
|
||||
|
||||
CompositionLocalProvider(LocalFullScreenNavHostController provides navHostController) {
|
||||
NavHost(
|
||||
navController = navHostController,
|
||||
startDestination = startDestination.route,
|
||||
) {
|
||||
composable(Screen.Authentication.route) {
|
||||
SystemThemeColor(
|
||||
systemUiController = systemUiController,
|
||||
statusDarkIcons = isSystemInDarkTheme().not(),
|
||||
navigationDarkIcons = isSystemInDarkTheme().not(),
|
||||
)
|
||||
AuthenticationScreen()
|
||||
}
|
||||
composable(Screen.Home.route) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.pixelized.biblib.ui.navigation.page
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import com.pixelized.biblib.R
|
||||
|
||||
sealed class Page(
|
||||
val route: String,
|
||||
val icon: ImageVector,
|
||||
@StringRes val label: Int,
|
||||
) {
|
||||
object News : Page(
|
||||
route = "news",
|
||||
icon = Icons.Default.NewReleases,
|
||||
label = R.string.menu_new,
|
||||
)
|
||||
|
||||
object Books : Page(
|
||||
route = "books",
|
||||
icon = Icons.Default.AutoStories,
|
||||
label = R.string.menu_book,
|
||||
)
|
||||
|
||||
object Series : Page(
|
||||
route = "series",
|
||||
icon = Icons.Default.AutoAwesomeMotion,
|
||||
label = R.string.menu_series,
|
||||
)
|
||||
|
||||
object Author : Page(
|
||||
route = "author",
|
||||
icon = Icons.Default.SupervisorAccount,
|
||||
label = R.string.menu_author,
|
||||
)
|
||||
|
||||
object Tag : Page(
|
||||
route = "tag",
|
||||
icon = Icons.Default.LocalOffer,
|
||||
label = R.string.menu_tag,
|
||||
)
|
||||
|
||||
companion object {
|
||||
val all = listOf(News, Books, Series, Author, Tag)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.pixelized.biblib.ui.navigation.page
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.pixelized.biblib.ui.screen.home.BooksPage
|
||||
import com.pixelized.biblib.ui.screen.home.NewsPage
|
||||
|
||||
val LocalPageNavHostController = compositionLocalOf<NavHostController> {
|
||||
error("LocalHomePageNavHostController is not ready yet.")
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PageNavHost(
|
||||
modifier: Modifier = Modifier,
|
||||
navHostController: NavHostController = rememberNavController(),
|
||||
startDestination: Page = Page.News
|
||||
) {
|
||||
CompositionLocalProvider(LocalPageNavHostController provides navHostController) {
|
||||
NavHost(
|
||||
modifier = modifier,
|
||||
navController = navHostController,
|
||||
startDestination = startDestination.route,
|
||||
) {
|
||||
composable(Page.News.route) {
|
||||
NewsPage()
|
||||
}
|
||||
composable(Page.Books.route) {
|
||||
BooksPage()
|
||||
}
|
||||
composable(Page.Series.route) {
|
||||
|
||||
}
|
||||
composable(Page.Author.route) {
|
||||
|
||||
}
|
||||
composable(Page.Tag.route) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.biblib.ui.navigation
|
||||
package com.pixelized.biblib.ui.navigation.screen
|
||||
|
||||
sealed class Screen(
|
||||
val route: String,
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.pixelized.biblib.ui.navigation.screen
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.pixelized.biblib.ui.screen.authentication.AuthenticationScreen
|
||||
import com.pixelized.biblib.ui.screen.home.HomeScreen
|
||||
|
||||
val LocalScreenNavHostController = compositionLocalOf<NavHostController> {
|
||||
error("LocalFullScreenNavHostController is not ready yet.")
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ScreenNavHost(
|
||||
modifier: Modifier = Modifier,
|
||||
navHostController: NavHostController = rememberNavController(),
|
||||
startDestination: Screen = Screen.Authentication
|
||||
) {
|
||||
CompositionLocalProvider(LocalScreenNavHostController provides navHostController) {
|
||||
NavHost(
|
||||
modifier = modifier,
|
||||
navController = navHostController,
|
||||
startDestination = startDestination.route,
|
||||
) {
|
||||
composable(Screen.Authentication.route) {
|
||||
AuthenticationScreen()
|
||||
}
|
||||
composable(Screen.Home.route) {
|
||||
HomeScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package com.pixelized.biblib.ui.old.composable.common
|
||||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CutCornerShape
|
||||
|
|
@ -21,14 +23,14 @@ import androidx.compose.ui.unit.dp
|
|||
import com.pixelized.biblib.BuildConfig
|
||||
import com.pixelized.biblib.R
|
||||
import com.pixelized.biblib.model.user.User
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
import com.pixelized.biblib.ui.old.viewmodel.user.IUserViewModel
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
import java.util.*
|
||||
|
||||
|
||||
@Composable
|
||||
fun BibLibDrawer(
|
||||
userViewModel : IUserViewModel,
|
||||
userViewModel: IUserViewModel,
|
||||
onNewClick: () -> Unit = {},
|
||||
onBookClick: () -> Unit = {},
|
||||
onSeriesClick: () -> Unit = {},
|
||||
|
|
@ -95,7 +97,7 @@ fun BibLibDrawer(
|
|||
style = typography.caption,
|
||||
text = stringResource(
|
||||
R.string.app_version,
|
||||
BuildConfig.BUILD_TYPE.toUpperCase(Locale.getDefault()),
|
||||
BuildConfig.BUILD_TYPE.uppercase(Locale.getDefault()),
|
||||
BuildConfig.VERSION_NAME,
|
||||
BuildConfig.VERSION_CODE
|
||||
)
|
||||
|
|
@ -106,30 +108,34 @@ fun BibLibDrawer(
|
|||
@Composable
|
||||
private fun DrawerItem(text: String, imageVector: ImageVector, onClick: () -> Unit) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(52.dp)
|
||||
.clickable { onClick() }) {
|
||||
.clickable { onClick() },
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
imageVector = imageVector,
|
||||
contentDescription = ""
|
||||
contentDescription = null,
|
||||
)
|
||||
Text(
|
||||
text = text,
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Spacer(
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Icon(
|
||||
modifier = Modifier.padding(end = 8.dp),
|
||||
imageVector = Icons.Default.NavigateNext,
|
||||
contentDescription = ""
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
|
||||
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
|
||||
fun BibLibDrawerLightPreview() {
|
||||
BibLibTheme {
|
||||
BibLibDrawer(
|
||||
|
|
|
|||
|
|
@ -66,23 +66,6 @@ private fun download(
|
|||
url: URL,
|
||||
): State<Painter> {
|
||||
val state = mutableStateOf(placeHolder)
|
||||
val cache = EntryPointAccessors
|
||||
.fromApplication(context, PersistenceModule::class.java)
|
||||
.provideBitmapCache(context)
|
||||
|
||||
coroutineScope.launch {
|
||||
val resource = cache.readFromDisk(url)?.let { BitmapPainter(it.asImageBitmap()) }
|
||||
if (resource != null) {
|
||||
state.value = resource
|
||||
} else {
|
||||
cache.download(url) { downloaded ->
|
||||
if (downloaded != null) {
|
||||
cache.writeToDisk(url, downloaded)
|
||||
state.value = BitmapPainter(downloaded.asImageBitmap())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.pixelized.biblib.ui.old.viewmodel.book
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
|
@ -14,6 +15,7 @@ import com.pixelized.biblib.network.client.IBibLibClient
|
|||
import com.pixelized.biblib.network.factory.BookFactory
|
||||
import com.pixelized.biblib.repository.apiCache.IAPICacheRepository
|
||||
import com.pixelized.biblib.repository.book.IBookRepository
|
||||
import com.pixelized.biblib.ui.composable.StateUio
|
||||
import com.pixelized.biblib.ui.old.data.BookThumbnailUio
|
||||
import com.pixelized.biblib.ui.old.data.BookUio
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
|
|
|||
|
|
@ -32,11 +32,11 @@ class NavigationViewModel : ViewModel(), INavigationViewModel {
|
|||
override fun navigateBack(): Boolean {
|
||||
return when (val navigable: Navigable? = popBackStack()) {
|
||||
is Page -> {
|
||||
_page.postValue(navigable)
|
||||
// _page.postValue(navigable)
|
||||
true
|
||||
}
|
||||
is Screen -> {
|
||||
_screen.postValue(navigable)
|
||||
// _screen.postValue(navigable)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.biblib.ui.authentication
|
||||
package com.pixelized.biblib.ui.screen.authentication
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.biblib.ui.authentication
|
||||
package com.pixelized.biblib.ui.screen.authentication
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
|
|
@ -32,12 +32,12 @@ 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.AnimatedDelayer
|
||||
import com.pixelized.biblib.ui.composable.AnimatedOffset
|
||||
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.navigation.LocalFullScreenNavHostController
|
||||
import com.pixelized.biblib.ui.navigation.Screen
|
||||
import com.pixelized.biblib.ui.navigation.screen.LocalScreenNavHostController
|
||||
import com.pixelized.biblib.ui.navigation.screen.Screen
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
import com.pixelized.biblib.ui.theme.color.GoogleColorPalette
|
||||
import com.pixelized.biblib.utils.extention.bibLib
|
||||
|
|
@ -46,7 +46,7 @@ import com.pixelized.biblib.utils.extention.bibLib
|
|||
fun AuthenticationScreen(
|
||||
viewModel: AuthenticationViewModel = hiltViewModel(),
|
||||
) {
|
||||
val navHostController = LocalFullScreenNavHostController.current
|
||||
val navHostController = LocalScreenNavHostController.current
|
||||
|
||||
AuthenticationScreenContent(
|
||||
login = viewModel.form.login,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.biblib.ui.authentication
|
||||
package com.pixelized.biblib.ui.screen.authentication
|
||||
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.pixelized.biblib.ui.screen.home
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.common.LazyBookThumbnailColumn
|
||||
import com.pixelized.biblib.ui.screen.home.viewModel.BooksViewModel
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
|
||||
@Composable
|
||||
fun BooksPage(
|
||||
booksViewModel: BooksViewModel = hiltViewModel()
|
||||
) {
|
||||
LazyBookThumbnailColumn(
|
||||
books = booksViewModel.books
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun BooksPagePreview() {
|
||||
BibLibTheme {
|
||||
Column {
|
||||
NewsPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
package com.pixelized.biblib.ui.screen.home
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.google.accompanist.insets.systemBarsPadding
|
||||
import com.pixelized.biblib.R
|
||||
import com.pixelized.biblib.ui.navigation.page.Page
|
||||
import com.pixelized.biblib.ui.navigation.page.PageNavHost
|
||||
|
||||
@Composable
|
||||
fun HomeScreen() {
|
||||
val pageNavHostController = rememberNavController()
|
||||
Scaffold(
|
||||
modifier = Modifier.systemBarsPadding(),
|
||||
topBar = {
|
||||
Column {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(text = stringResource(id = R.string.app_name))
|
||||
}
|
||||
)
|
||||
BottomBarNavigation(
|
||||
homePageNavController = pageNavHostController
|
||||
)
|
||||
}
|
||||
},
|
||||
content = {
|
||||
PageNavHost(
|
||||
modifier = Modifier.padding(it),
|
||||
navHostController = pageNavHostController,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BottomBarNavigation(
|
||||
homePageNavController: NavHostController,
|
||||
pages: List<Page> = Page.all
|
||||
) {
|
||||
BottomNavigation(
|
||||
backgroundColor = MaterialTheme.colors.background,
|
||||
) {
|
||||
val navBackStackEntry by homePageNavController.currentBackStackEntryAsState()
|
||||
val currentDestination = navBackStackEntry?.destination
|
||||
|
||||
pages.forEach { page ->
|
||||
val selected = currentDestination?.hierarchy?.any { it.route == page.route }
|
||||
BottomNavigationIcon(
|
||||
page = page,
|
||||
selected = selected ?: false,
|
||||
onClick = {
|
||||
homePageNavController.navigate(page.route) {
|
||||
// Pop up to the start destination of the graph to avoid building up a
|
||||
// large stack of destinations on the back stack as users select items
|
||||
popUpTo(homePageNavController.graph.findStartDestination().id) {
|
||||
saveState = true
|
||||
}
|
||||
// Avoid multiple copies of the same destination when re-selecting the same item
|
||||
launchSingleTop = true
|
||||
// Restore state when re-selecting a previously selected item
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RowScope.BottomNavigationIcon(
|
||||
modifier: Modifier = Modifier,
|
||||
page: Page,
|
||||
selected: Boolean,
|
||||
selectedColor: Color = MaterialTheme.colors.primary,
|
||||
defaultColor: Color = MaterialTheme.colors.onSurface,
|
||||
onClick: () -> Unit = {},
|
||||
) {
|
||||
BottomNavigationItem(
|
||||
modifier = modifier,
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = page.icon,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
style = MaterialTheme.typography.caption,
|
||||
text = stringResource(id = page.label),
|
||||
)
|
||||
},
|
||||
selectedContentColor = selectedColor,
|
||||
unselectedContentColor = defaultColor,
|
||||
selected = selected,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.pixelized.biblib.ui.screen.home
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.common.LazyBookThumbnailColumn
|
||||
import com.pixelized.biblib.ui.screen.home.viewModel.BooksViewModel
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
|
||||
@Composable
|
||||
fun NewsPage(
|
||||
booksViewModel: BooksViewModel = hiltViewModel()
|
||||
) {
|
||||
LazyBookThumbnailColumn(
|
||||
books = booksViewModel.news
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
private fun NewPagePreview() {
|
||||
BibLibTheme {
|
||||
Column {
|
||||
NewsPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
package com.pixelized.biblib.ui.screen.home.common
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.items
|
||||
import com.pixelized.biblib.R
|
||||
import com.pixelized.biblib.ui.screen.home.item.BookThumbnail
|
||||
import com.pixelized.biblib.ui.screen.home.uio.BookThumbnailUio
|
||||
import com.pixelized.biblib.ui.screen.home.uio.CoverUio
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
import com.pixelized.biblib.utils.extention.bibLib
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
@Composable
|
||||
fun LazyBookThumbnailColumn(
|
||||
modifier: Modifier = Modifier,
|
||||
state: LazyListState = rememberLazyListState(),
|
||||
books: LazyPagingItems<BookThumbnailUio>,
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.bibLib.dimen.small),
|
||||
contentPadding = PaddingValues(all = MaterialTheme.bibLib.dimen.small),
|
||||
state = state,
|
||||
) {
|
||||
items(books) { thumbnail ->
|
||||
BookThumbnail(thumbnail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun LazyBookThumbnailColumnPreview() {
|
||||
BibLibTheme {
|
||||
LazyBookThumbnailColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
books = previewResources(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun previewResources(): LazyPagingItems<BookThumbnailUio> {
|
||||
val cover = CoverUio(
|
||||
painter = painterResource(id = R.drawable.ic_baseline_auto_stories_24),
|
||||
contentScale = ContentScale.None,
|
||||
)
|
||||
val thumbnails = listOf(
|
||||
BookThumbnailUio(
|
||||
id = 112,
|
||||
title = "Prélude à Fondation",
|
||||
author = "Asimov",
|
||||
date = "1988",
|
||||
genre = "Sci-Fi",
|
||||
isNew = false,
|
||||
cover = remember { mutableStateOf(cover) },
|
||||
),
|
||||
BookThumbnailUio(
|
||||
id = 78,
|
||||
title = "L'Aube de Fondation",
|
||||
author = "Asimov",
|
||||
date = "1993",
|
||||
genre = "Sci-Fi",
|
||||
isNew = false,
|
||||
cover = remember { mutableStateOf(cover) },
|
||||
),
|
||||
BookThumbnailUio(
|
||||
id = 90,
|
||||
title = "Fondation",
|
||||
author = "Asimov",
|
||||
date = "1951",
|
||||
genre = "Sci-Fi",
|
||||
isNew = false,
|
||||
cover = remember { mutableStateOf(cover) },
|
||||
),
|
||||
BookThumbnailUio(
|
||||
id = 184,
|
||||
title = "Fondation et Empire",
|
||||
author = "Asimov",
|
||||
date = "1952",
|
||||
genre = "Sci-Fi",
|
||||
isNew = false,
|
||||
cover = remember { mutableStateOf(cover) },
|
||||
),
|
||||
BookThumbnailUio(
|
||||
id = 185,
|
||||
title = "Seconde Fondation",
|
||||
author = "Asimov",
|
||||
date = "1953",
|
||||
genre = "Sci-Fi",
|
||||
isNew = false,
|
||||
cover = remember { mutableStateOf(cover) },
|
||||
),
|
||||
BookThumbnailUio(
|
||||
id = 119,
|
||||
title = "Fondation foudroyée",
|
||||
author = "Asimov",
|
||||
date = "1982",
|
||||
genre = "Sci-Fi",
|
||||
isNew = false,
|
||||
cover = remember { mutableStateOf(cover) },
|
||||
),
|
||||
BookThumbnailUio(
|
||||
id = 163,
|
||||
title = "Terre et Fondation",
|
||||
author = "Asimov",
|
||||
date = "1986",
|
||||
genre = "Sci-Fi",
|
||||
isNew = false,
|
||||
cover = remember { mutableStateOf(cover) },
|
||||
),
|
||||
)
|
||||
return flowOf(PagingData.from(thumbnails)).collectAsLazyPagingItems()
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
package com.pixelized.biblib.ui.screen.home.item
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.pixelized.biblib.R
|
||||
import com.pixelized.biblib.ui.screen.home.uio.BookThumbnailUio
|
||||
import com.pixelized.biblib.ui.screen.home.uio.CoverUio
|
||||
import com.pixelized.biblib.ui.theme.BibLibTheme
|
||||
import com.pixelized.biblib.utils.extention.bibLib
|
||||
|
||||
@Composable
|
||||
fun BookThumbnail(
|
||||
thumbnail: BookThumbnailUio?,
|
||||
onClick: (BookThumbnailUio) -> Unit = { },
|
||||
) {
|
||||
val currentOnClick by rememberUpdatedState(newValue = onClick)
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.clickable(enabled = thumbnail != null) {
|
||||
thumbnail?.let { currentOnClick(it) }
|
||||
}
|
||||
.wrapContentHeight(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.height(MaterialTheme.bibLib.dimen.thumbnail.cover.height),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
thumbnail?.cover?.let { it ->
|
||||
Cover(
|
||||
modifier = Modifier.size(MaterialTheme.bibLib.dimen.thumbnail.cover),
|
||||
image = it,
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(MaterialTheme.bibLib.dimen.small)
|
||||
) {
|
||||
Text(
|
||||
style = MaterialTheme.typography.body1,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
text = thumbnail?.title ?: "",
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
Text(
|
||||
style = MaterialTheme.typography.caption,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
text = thumbnail?.author ?: ""
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
style = MaterialTheme.typography.caption,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
text = thumbnail?.genre ?: ""
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Text(
|
||||
style = MaterialTheme.typography.caption,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
text = thumbnail?.date ?: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
private fun Cover(
|
||||
modifier: Modifier = Modifier,
|
||||
image: State<CoverUio>
|
||||
) {
|
||||
val cover by image
|
||||
AnimatedContent(
|
||||
targetState = cover,
|
||||
transitionSpec = { fadeIn() with fadeOut() }
|
||||
) {
|
||||
Image(
|
||||
modifier = modifier,
|
||||
alignment = Alignment.Center,
|
||||
contentScale = it.contentScale,
|
||||
painter = it.painter,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
private fun BookThumbnailPreview() {
|
||||
val painter = painterResource(id = R.drawable.ic_baseline_auto_stories_24)
|
||||
val thumbnail = BookThumbnailUio(
|
||||
id = 0,
|
||||
genre = "Sci-Fi",
|
||||
title = "Foundation",
|
||||
author = "Asimov",
|
||||
date = "1951",
|
||||
isNew = false,
|
||||
cover = remember {
|
||||
mutableStateOf(
|
||||
CoverUio(
|
||||
painter = painter,
|
||||
contentScale = ContentScale.None,
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
BibLibTheme {
|
||||
BookThumbnail(thumbnail = thumbnail)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.pixelized.biblib.ui.screen.home.uio
|
||||
|
||||
import androidx.compose.runtime.State
|
||||
|
||||
class BookThumbnailUio(
|
||||
val id: Int,
|
||||
val genre: String,
|
||||
val title: String,
|
||||
val author: String,
|
||||
val date: String?,
|
||||
val isNew: Boolean,
|
||||
val cover: State<CoverUio>,
|
||||
)
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.pixelized.biblib.ui.screen.home.uio
|
||||
|
||||
import com.pixelized.biblib.network.client.IBibLibClient.Companion.COVER_URL
|
||||
import java.net.URL
|
||||
|
||||
data class BookUio(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val author: String,
|
||||
val rating: Float,
|
||||
val language: String,
|
||||
val date: String?,
|
||||
val series: String?,
|
||||
val description: String,
|
||||
) {
|
||||
val cover: URL = URL("${COVER_URL}/$id.jpg")
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.pixelized.biblib.ui.screen.home.uio
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
|
||||
@Stable
|
||||
@Immutable
|
||||
data class CoverUio(
|
||||
val contentScale: ContentScale = ContentScale.FillBounds,
|
||||
val painter: Painter,
|
||||
)
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
package com.pixelized.biblib.ui.screen.home.viewModel
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.google.accompanist.drawablepainter.DrawablePainter
|
||||
import com.pixelized.biblib.R
|
||||
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.repository.apiCache.IAPICacheRepository
|
||||
import com.pixelized.biblib.repository.book.IBookRepository
|
||||
import com.pixelized.biblib.ui.composable.StateUio
|
||||
import com.pixelized.biblib.ui.screen.home.uio.BookThumbnailUio
|
||||
import com.pixelized.biblib.ui.screen.home.uio.BookUio
|
||||
import com.pixelized.biblib.ui.screen.home.uio.CoverUio
|
||||
import com.pixelized.biblib.utils.BitmapCache
|
||||
import com.pixelized.biblib.utils.extention.capitalize
|
||||
import com.pixelized.biblib.utils.extention.context
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.net.URL
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
/**
|
||||
* TODO: there is some book related code that should be inside a Repository // DataSource.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class BooksViewModel @Inject constructor(
|
||||
application: Application,
|
||||
private val bookRepository: IBookRepository,
|
||||
private val client: IBibLibClient,
|
||||
private val apiCache: IAPICacheRepository,
|
||||
private val cache: BitmapCache,
|
||||
) : AndroidViewModel(application) {
|
||||
private val formatterLong = SimpleDateFormat("MMMM yyyy", Locale.getDefault())
|
||||
private val formatterShort = SimpleDateFormat("MMM yyyy", Locale.getDefault())
|
||||
|
||||
private val _state = mutableStateOf<StateUio<Boolean>?>(null)
|
||||
val state: State<StateUio<Boolean>?> = _state
|
||||
|
||||
private val booksSource = Pager(
|
||||
config = PagingConfig(pageSize = PAGING_SIZE),
|
||||
pagingSourceFactory = bookRepository.getBooksSource()
|
||||
.map { it.toThumbnail() }
|
||||
.asPagingSourceFactory(Dispatchers.IO)
|
||||
).flow
|
||||
val books @Composable get() = booksSource.collectAsLazyPagingItems()
|
||||
|
||||
private val newsSource: Flow<PagingData<BookThumbnailUio>> = Pager(
|
||||
config = PagingConfig(pageSize = PAGING_SIZE),
|
||||
pagingSourceFactory = bookRepository.getNewsSource()
|
||||
.map { it.toThumbnail() }
|
||||
.asPagingSourceFactory(Dispatchers.IO)
|
||||
).flow
|
||||
val news @Composable get() = newsSource.collectAsLazyPagingItems()
|
||||
|
||||
fun updateBooks() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
_state.value = StateUio.Progress()
|
||||
try {
|
||||
val updated = loadNewBooks() && loadAllBooks()
|
||||
_state.value = StateUio.Success(updated)
|
||||
} catch (exception: Exception) {
|
||||
Log.e("BooksViewModel", exception.message, exception)
|
||||
_state.value = StateUio.Failure(exception = exception)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getBookDetail(id: Int): State<BookUio?> {
|
||||
val data = mutableStateOf<BookUio?>(null)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val factory = BookFactory()
|
||||
val response = client.service.detail(id)
|
||||
val book = factory.fromDetailResponseToBook(response)
|
||||
data.value = book.toUio()
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
fun send(id: Int, mail: String) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
client.service.send(bookId = id, mail = mail)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
//////////////////////////////////////
|
||||
// region: UIO conversion
|
||||
|
||||
private fun Book.toThumbnail() = BookThumbnailUio(
|
||||
id = id,
|
||||
genre = genre?.joinToString { it.name } ?: "",
|
||||
title = title,
|
||||
author = author.joinToString { it.name },
|
||||
date = if (releaseDate.time < 0) {
|
||||
null
|
||||
} else {
|
||||
formatterLong.format(releaseDate).capitalize()
|
||||
},
|
||||
isNew = isNew,
|
||||
cover = cover(url = URL("${IBibLibClient.THUMBNAIL_URL}/$id.jpg"))
|
||||
)
|
||||
|
||||
private fun Book.toUio() = BookUio(
|
||||
id = id,
|
||||
title = title,
|
||||
author = author.joinToString { it.name },
|
||||
rating = rating?.toFloat() ?: 0.0f,
|
||||
language = language?.displayLanguage?.capitalize() ?: "",
|
||||
date = if (releaseDate.time < 0) {
|
||||
null
|
||||
} else {
|
||||
formatterShort.format(releaseDate).capitalize()
|
||||
},
|
||||
series = series?.name,
|
||||
description = synopsis ?: "",
|
||||
)
|
||||
|
||||
// endregion
|
||||
|
||||
private fun cover(
|
||||
cache: BitmapCache = this.cache,
|
||||
placeHolder: Painter = DrawablePainter(
|
||||
drawable = AppCompatResources.getDrawable(
|
||||
context,
|
||||
R.drawable.ic_baseline_auto_stories_24
|
||||
)!!
|
||||
),
|
||||
coroutineScope: CoroutineScope = viewModelScope,
|
||||
url: URL,
|
||||
): State<CoverUio> {
|
||||
if (cache.exist(url)) {
|
||||
val bitmap = cache.readFromDisk(url) ?: throw RuntimeException("")
|
||||
val resource = BitmapPainter(bitmap.asImageBitmap())
|
||||
return mutableStateOf(CoverUio(painter = resource))
|
||||
} else {
|
||||
val state = mutableStateOf(
|
||||
CoverUio(
|
||||
contentScale = ContentScale.None,
|
||||
painter = placeHolder
|
||||
)
|
||||
)
|
||||
coroutineScope.launch {
|
||||
val resource = cache.readFromDisk(url)?.let { BitmapPainter(it.asImageBitmap()) }
|
||||
if (resource != null) {
|
||||
state.value = CoverUio(painter = resource)
|
||||
} else {
|
||||
val downloaded = cache.download(url)
|
||||
if (downloaded != null) {
|
||||
cache.writeToDisk(url, downloaded)
|
||||
state.value = CoverUio(painter = BitmapPainter(downloaded.asImageBitmap()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PAGING_SIZE = 30
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.biblib.ui.launch
|
||||
package com.pixelized.biblib.ui.screen.launch
|
||||
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
|
@ -5,6 +5,8 @@ import androidx.compose.runtime.Stable
|
|||
import androidx.compose.runtime.compositionLocalOf
|
||||
import com.pixelized.biblib.ui.theme.color.BibLibColor
|
||||
import com.pixelized.biblib.ui.theme.dimen.BibLibDimen
|
||||
import com.pixelized.biblib.ui.theme.shape.BibLibShape
|
||||
import com.pixelized.biblib.ui.theme.typography.BibLibTypography
|
||||
|
||||
val LocalBibLibTheme = compositionLocalOf<BibLibTheme> {
|
||||
error("BibLibTheme not ready yet.")
|
||||
|
|
@ -13,6 +15,8 @@ val LocalBibLibTheme = compositionLocalOf<BibLibTheme> {
|
|||
@Stable
|
||||
@Immutable
|
||||
data class BibLibTheme(
|
||||
val dimen: BibLibDimen,
|
||||
val color: BibLibColor,
|
||||
val dimen: BibLibDimen,
|
||||
val typography: BibLibTypography,
|
||||
val shape: BibLibShape,
|
||||
)
|
||||
|
|
@ -7,8 +7,8 @@ import androidx.compose.runtime.CompositionLocalProvider
|
|||
import com.pixelized.biblib.ui.theme.color.bibLibDarkColors
|
||||
import com.pixelized.biblib.ui.theme.color.bibLibLightColors
|
||||
import com.pixelized.biblib.ui.theme.dimen.BibLibDimen
|
||||
import com.pixelized.biblib.ui.theme.shape.Shapes
|
||||
import com.pixelized.biblib.ui.theme.typography.Typography
|
||||
import com.pixelized.biblib.ui.theme.shape.BibLibShape
|
||||
import com.pixelized.biblib.ui.theme.typography.BibLibTypography
|
||||
|
||||
@Composable
|
||||
fun BibLibTheme(
|
||||
|
|
@ -16,15 +16,16 @@ fun BibLibTheme(
|
|||
content: @Composable () -> Unit
|
||||
) {
|
||||
val theme = BibLibTheme(
|
||||
color = if (darkTheme) bibLibDarkColors() else bibLibLightColors(),
|
||||
dimen = BibLibDimen(),
|
||||
color = if (darkTheme) bibLibDarkColors() else bibLibLightColors()
|
||||
typography = BibLibTypography(),
|
||||
shape = BibLibShape(),
|
||||
)
|
||||
|
||||
CompositionLocalProvider(LocalBibLibTheme provides theme) {
|
||||
MaterialTheme(
|
||||
colors = theme.color.base,
|
||||
typography = Typography,
|
||||
shapes = Shapes,
|
||||
typography = theme.typography.base,
|
||||
shapes = theme.shape.base,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.pixelized.biblib.ui.theme.dimen
|
|||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Stable
|
||||
|
|
@ -14,14 +15,21 @@ data class BibLibDimen(
|
|||
val medium: Dp = 16.dp,
|
||||
val large: Dp = 32.dp,
|
||||
val extraLarge: Dp = 64.dp,
|
||||
val dialog: Dialog = Dialog()
|
||||
val dialog: Dialog = Dialog(),
|
||||
val thumbnail: BookThumbnail = BookThumbnail(),
|
||||
) {
|
||||
@Stable
|
||||
@Immutable
|
||||
data class Dialog(
|
||||
val minimum: DpSize = DpSize(240.dp, 120.dp),
|
||||
val elevation: Dp = 4.dp,
|
||||
val minWidth: Dp = 240.dp,
|
||||
val minHeight: Dp = 120.dp,
|
||||
val iconSize: Dp = 52.dp,
|
||||
)
|
||||
|
||||
@Stable
|
||||
@Immutable
|
||||
data class BookThumbnail(
|
||||
val cover: DpSize = DpSize(60.dp, 96.dp),
|
||||
val corner: Dp = 8.dp,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.pixelized.biblib.ui.theme.shape
|
||||
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Shapes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Stable
|
||||
@Immutable
|
||||
data class BibLibShape(
|
||||
val base: Shapes = Shapes(
|
||||
small = RoundedCornerShape(50),
|
||||
medium = RoundedCornerShape(4.dp),
|
||||
large = RoundedCornerShape(0.dp),
|
||||
),
|
||||
)
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
package com.pixelized.biblib.ui.theme.shape
|
||||
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Shapes
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
val Shapes = Shapes(
|
||||
small = RoundedCornerShape(50),
|
||||
medium = RoundedCornerShape(4.dp),
|
||||
large = RoundedCornerShape(0.dp)
|
||||
)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.pixelized.biblib.ui.theme.typography
|
||||
|
||||
import androidx.compose.material.Typography
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
|
||||
@Stable
|
||||
@Immutable
|
||||
data class BibLibTypography(
|
||||
val base: Typography = Typography()
|
||||
)
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package com.pixelized.biblib.ui.theme.typography
|
||||
|
||||
import androidx.compose.material.Typography
|
||||
|
||||
val Typography = Typography()
|
||||
|
|
@ -9,13 +9,13 @@ import kotlinx.coroutines.withContext
|
|||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import javax.inject.Inject
|
||||
|
||||
class BitmapCache(context: Context) {
|
||||
class BitmapCache @Inject constructor(context: Context) {
|
||||
private var cache: File? = context.cacheDir
|
||||
|
||||
fun writeToDisk(url: URL, bitmap: Bitmap) {
|
||||
val path = cache?.absolutePath + url.file
|
||||
val file = File(path)
|
||||
val file = file(url)
|
||||
try {
|
||||
file.mkdirs()
|
||||
file.delete()
|
||||
|
|
@ -26,8 +26,7 @@ class BitmapCache(context: Context) {
|
|||
}
|
||||
|
||||
fun readFromDisk(url: URL): Bitmap? {
|
||||
val path = cache?.absolutePath + url.file
|
||||
val file = File(path)
|
||||
val file = file(url)
|
||||
return try {
|
||||
BitmapFactory.decodeStream(file.inputStream())
|
||||
} catch (e: IOException) {
|
||||
|
|
@ -35,8 +34,14 @@ class BitmapCache(context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun download(url: URL, callback: suspend (Bitmap?) -> Unit) {
|
||||
val bitmap = withContext(Dispatchers.IO) {
|
||||
fun exist(url: URL): Boolean {
|
||||
val file = file(url)
|
||||
return file.exists()
|
||||
}
|
||||
|
||||
suspend fun download(url: URL): Bitmap? {
|
||||
Log.v("BitmapCache","download: $url")
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
BitmapFactory.decodeStream(url.openStream())
|
||||
} catch (e: IOException) {
|
||||
|
|
@ -44,6 +49,7 @@ class BitmapCache(context: Context) {
|
|||
null
|
||||
}
|
||||
}
|
||||
callback.invoke(bitmap)
|
||||
}
|
||||
|
||||
private fun file(url: URL): File = File(cache?.absolutePath + url.file)
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.pixelized.biblib.utils.extention
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
|
||||
val AndroidViewModel.context: Context get() = this.getApplication()
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.pixelized.biblib.utils.extention
|
||||
|
||||
import java.util.*
|
||||
|
||||
fun String.capitalize() = this.replaceFirstChar {
|
||||
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,1l-5,5v11l5,-4.5L19,1zM1,6v14.65c0,0.25 0.25,0.5 0.5,0.5 0.1,0 0.15,-0.05 0.25,-0.05C3.1,20.45 5.05,20 6.5,20c1.95,0 4.05,0.4 5.5,1.5L12,6c-1.45,-1.1 -3.55,-1.5 -5.5,-1.5S2.45,4.9 1,6zM23,19.5L23,6c-0.6,-0.45 -1.25,-0.75 -2,-1v13.5c-1.1,-0.35 -2.3,-0.5 -3.5,-0.5 -1.7,0 -4.15,0.65 -5.5,1.5v2c1.35,-0.85 3.8,-1.5 5.5,-1.5 1.65,0 3.35,0.3 4.75,1.05 0.1,0.05 0.15,0.05 0.25,0.05 0.25,0 0.5,-0.25 0.5,-0.5v-1.1z" />
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/black"
|
||||
android:pathData="M19,1l-5,5v11l5,-4.5L19,1zM1,6v14.65c0,0.25 0.25,0.5 0.5,0.5 0.1,0 0.15,-0.05 0.25,-0.05C3.1,20.45 5.05,20 6.5,20c1.95,0 4.05,0.4 5.5,1.5L12,6c-1.45,-1.1 -3.55,-1.5 -5.5,-1.5S2.45,4.9 1,6zM23,19.5L23,6c-0.6,-0.45 -1.25,-0.75 -2,-1v13.5c-1.1,-0.35 -2.3,-0.5 -3.5,-0.5 -1.7,0 -4.15,0.65 -5.5,1.5v2c1.35,-0.85 3.8,-1.5 5.5,-1.5 1.65,0 3.35,0.3 4.75,1.05 0.1,0.05 0.15,0.05 0.25,0.05 0.25,0 0.5,-0.25 0.5,-0.5v-1.1z" />
|
||||
</vector>
|
||||
50
app/src/main/res/values-fr/strings.xml
Normal file
50
app/src/main/res/values-fr/strings.xml
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<resources>
|
||||
<string name="app_name">BibLib</string>
|
||||
<string name="app_version">%1$s: %2$s - %3$d</string>
|
||||
|
||||
<!-- Actions -->
|
||||
|
||||
<string name="action_register">Register</string>
|
||||
<string name="action_login">Login</string>
|
||||
<string name="action_epub">EPUB</string>
|
||||
<string name="action_mobi">MOBI</string>
|
||||
<string name="action_send">SEND</string>
|
||||
<string name="action_google_sign_in">Sign in with</string>
|
||||
|
||||
<!-- Bottom bar -->
|
||||
|
||||
<!-- <string name="menu_new">Nouveautés</string>-->
|
||||
<!-- <string name="menu_book">Livres</string>-->
|
||||
<!-- <string name="menu_series">Séries</string>-->
|
||||
<!-- <string name="menu_author">Auteurs</string>-->
|
||||
<!-- <string name="menu_tag">Étiquettes</string>-->
|
||||
|
||||
<!-- Dialogs -->
|
||||
|
||||
<string name="error_generic">Oops!</string>
|
||||
<string name="error_authentication">Oops, connection failed!</string>
|
||||
<string name="error_book">Oops! library download failed!</string>
|
||||
|
||||
<string name="loading_authentication">Entering the Imperial Library of Trantor.</string>
|
||||
<string name="loading_book">Downloading the Imperial Library of Trantor.</string>
|
||||
|
||||
<string name="success_authentication">Authentication successful</string>
|
||||
<string name="success_book">Library successfully loaded</string>
|
||||
|
||||
<!-- Screens & pages -->
|
||||
|
||||
<string name="splash_welcome">Welcome to</string>
|
||||
|
||||
<string name="authentication_title">Sign in to BibLib</string>
|
||||
<string name="authentication_login">Login</string>
|
||||
<string name="authentication_password">Password</string>
|
||||
<string name="authentication_credential_remember">Remember my credential</string>
|
||||
|
||||
<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>
|
||||
|
||||
<string name="not_implemented_yet">Not implemented yet.</string>
|
||||
</resources>
|
||||
|
|
@ -11,6 +11,14 @@
|
|||
<string name="action_send">SEND</string>
|
||||
<string name="action_google_sign_in">Sign in with</string>
|
||||
|
||||
<!-- Menu item -->
|
||||
|
||||
<string name="menu_new">News</string>
|
||||
<string name="menu_book">Books</string>
|
||||
<string name="menu_series">Series</string>
|
||||
<string name="menu_author">Authors</string>
|
||||
<string name="menu_tag">tags</string>
|
||||
|
||||
<!-- Dialogs -->
|
||||
|
||||
<string name="error_generic">Oops!</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue