Change to Toolbar layout

This commit is contained in:
Thomas Andres Gomez 2022-04-25 00:08:55 +02:00
parent 7af9eb8acc
commit db7d6d8ab8
25 changed files with 544 additions and 157 deletions

View file

@ -1,6 +1,6 @@
package com.pixelized.biblib.model.user package com.pixelized.biblib.model.user
import java.util.Date import java.util.*
data class User( data class User(
val id: String, val id: String,
@ -10,4 +10,11 @@ data class User(
val firstname: String?, val firstname: String?,
val lastname: String?, val lastname: String?,
val amazonEmails: List<String>, val amazonEmails: List<String>,
val downloadedBooks: List<DownloadedBooks>,
)
data class DownloadedBooks(
val bookId: Int,
val date: Date,
val format: String,
) )

View file

@ -4,7 +4,8 @@ import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.room.Room import androidx.room.Room
import com.pixelized.biblib.database.BibLibDatabase import com.pixelized.biblib.database.BibLibDatabase
import com.pixelized.biblib.utils.CoverCache import com.pixelized.biblib.utils.cache.BookCoverCache
import com.pixelized.biblib.utils.cache.ImageCache
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -31,10 +32,22 @@ class PersistenceModule {
@Provides @Provides
@Singleton @Singleton
fun provideBitmapCache( fun provideBookCoverCache(
imageCache: ImageCache,
): BookCoverCache {
return BookCoverCache(
cache = imageCache
)
}
@Provides
@Singleton
fun provideImageCache(
@ApplicationContext context: Context, @ApplicationContext context: Context,
): CoverCache { ): ImageCache {
return CoverCache(context) return ImageCache(
context = context
)
} }
@Provides @Provides

View file

@ -11,5 +11,6 @@ interface IBibLibClient {
const val THUMBNAIL_URL = "$BASE_URL/api/book/thumbnail" const val THUMBNAIL_URL = "$BASE_URL/api/book/thumbnail"
const val COVER_URL = "$BASE_URL/api/book/cover" const val COVER_URL = "$BASE_URL/api/book/cover"
const val REGISTER_URL = "$BASE_URL/signup" const val REGISTER_URL = "$BASE_URL/signup"
const val EDIT_PROFILE = "$BASE_URL/profile"
} }
} }

View file

@ -1,5 +1,6 @@
package com.pixelized.biblib.network.factory package com.pixelized.biblib.network.factory
import com.pixelized.biblib.model.user.DownloadedBooks
import com.pixelized.biblib.model.user.User import com.pixelized.biblib.model.user.User
import com.pixelized.biblib.network.data.response.UserResponse import com.pixelized.biblib.network.data.response.UserResponse
import com.pixelized.biblib.utils.exception.MandatoryFieldMissingException import com.pixelized.biblib.utils.exception.MandatoryFieldMissingException
@ -23,6 +24,21 @@ class UserFactory {
val lastname = response.data?.local?.lastname val lastname = response.data?.local?.lastname
val amazonEmails = response.data?.local?.amazonEmails val amazonEmails = response.data?.local?.amazonEmails
val downloadedBooks = response.data?.history?.downloadedBooks?.mapNotNull { book ->
val bookId = book.id
val date = book.date?.let { parser.parse(it) }
val format = book.data?.data_format
if (bookId != null && date != null && format != null) {
DownloadedBooks(
bookId = bookId,
date = date,
format = format,
)
} else {
null
}
}
return User( return User(
id = id ?: throw error("id"), id = id ?: throw error("id"),
created = created ?: throw error("created"), created = created ?: throw error("created"),
@ -31,7 +47,7 @@ class UserFactory {
firstname = firstname, firstname = firstname,
lastname = lastname, lastname = lastname,
amazonEmails = amazonEmails ?: listOf(), amazonEmails = amazonEmails ?: listOf(),
downloadedBooks = downloadedBooks ?: listOf()
) )
} }
} }

View file

@ -2,6 +2,7 @@ package com.pixelized.biblib.repository.googleSignIn
import android.app.Application import android.app.Application
import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.auth.api.signin.GoogleSignInClient import com.google.android.gms.auth.api.signin.GoogleSignInClient
import com.google.android.gms.auth.api.signin.GoogleSignInOptions import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.pixelized.biblib.R import com.pixelized.biblib.R
@ -19,6 +20,10 @@ class GoogleSingInRepository(application: Application) : IGoogleSingInRepository
GoogleSignIn.getClient(application, option) GoogleSignIn.getClient(application, option)
} }
override val account: GoogleSignInAccount? by lazy {
GoogleSignIn.getLastSignedInAccount(application)
}
override val lastGoogleToken: String? by lazy { override val lastGoogleToken: String? by lazy {
GoogleSignIn.getLastSignedInAccount(application)?.idToken GoogleSignIn.getLastSignedInAccount(application)?.idToken
} }

View file

@ -1,10 +1,12 @@
package com.pixelized.biblib.repository.googleSignIn package com.pixelized.biblib.repository.googleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.auth.api.signin.GoogleSignInClient import com.google.android.gms.auth.api.signin.GoogleSignInClient
import com.google.android.gms.auth.api.signin.GoogleSignInOptions import com.google.android.gms.auth.api.signin.GoogleSignInOptions
interface IGoogleSingInRepository { interface IGoogleSingInRepository {
val option: GoogleSignInOptions val option: GoogleSignInOptions
val client: GoogleSignInClient val client: GoogleSignInClient
val account: GoogleSignInAccount?
val lastGoogleToken: String? val lastGoogleToken: String?
} }

View file

@ -15,6 +15,11 @@ sealed class Screen(
route = "home" route = "home"
) )
object Profile: Screen(
route = "profile"
)
@Deprecated(message = "User BottomDetailState instead.")
class BookDetail(id: Int) : Screen( class BookDetail(id: Int) : Screen(
route = "$ROUTE/$id" route = "$ROUTE/$id"
) { ) {

View file

@ -10,6 +10,7 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.pixelized.biblib.ui.screen.authentication.AuthenticationScreen import com.pixelized.biblib.ui.screen.authentication.AuthenticationScreen
import com.pixelized.biblib.ui.screen.home.HomeScreen import com.pixelized.biblib.ui.screen.home.HomeScreen
import com.pixelized.biblib.ui.screen.profile.ProfileScreen
val LocalScreenNavHostController = compositionLocalOf<NavHostController> { val LocalScreenNavHostController = compositionLocalOf<NavHostController> {
error("LocalFullScreenNavHostController is not ready yet.") error("LocalFullScreenNavHostController is not ready yet.")
@ -35,6 +36,9 @@ fun ScreenNavHost(
composable(Screen.Home.route) { composable(Screen.Home.route) {
HomeScreen() HomeScreen()
} }
composable(Screen.Profile.route) {
ProfileScreen()
}
} }
} }
} }
@ -44,4 +48,8 @@ fun NavHostController.navigateToHome() {
launchSingleTop = true launchSingleTop = true
popUpTo(0) { inclusive = true } popUpTo(0) { inclusive = true }
} }
}
fun NavHostController.navigateToProfile() {
navigate(Screen.Profile.route)
} }

View file

@ -18,7 +18,7 @@ import com.pixelized.biblib.ui.navigation.screen.Screen
import com.pixelized.biblib.ui.screen.home.common.uio.BookUio import com.pixelized.biblib.ui.screen.home.common.uio.BookUio
import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio
import com.pixelized.biblib.ui.screen.home.common.viewModel.ACoverViewModel import com.pixelized.biblib.ui.screen.home.common.viewModel.ACoverViewModel
import com.pixelized.biblib.utils.CoverCache import com.pixelized.biblib.utils.cache.BookCoverCache
import com.pixelized.biblib.utils.extention.capitalize import com.pixelized.biblib.utils.extention.capitalize
import com.pixelized.biblib.utils.extention.context import com.pixelized.biblib.utils.extention.context
import com.pixelized.biblib.utils.extention.shortDate import com.pixelized.biblib.utils.extention.shortDate
@ -32,10 +32,10 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class BookDetailViewModel @Inject constructor( class BookDetailViewModel @Inject constructor(
application: Application, application: Application,
coverCache: CoverCache, bookCoverCache: BookCoverCache,
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
private val client: IBibLibClient, private val client: IBibLibClient,
) : ACoverViewModel(application, coverCache) { ) : ACoverViewModel(application, bookCoverCache) {
private val _state = mutableStateOf<StateUio<BookUio>>(StateUio.Progress()) private val _state = mutableStateOf<StateUio<BookUio>>(StateUio.Progress())
val state: State<StateUio<BookUio>> get() = _state val state: State<StateUio<BookUio>> get() = _state

View file

@ -8,9 +8,11 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.* import androidx.compose.material.Button
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.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Download import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Send import androidx.compose.material.icons.filled.Send
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -39,6 +41,7 @@ import com.pixelized.biblib.ui.screen.home.common.uio.BookUio
import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio
import com.pixelized.biblib.ui.theme.BibLibTheme import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.utils.extention.bibLib import com.pixelized.biblib.utils.extention.bibLib
import com.pixelized.biblib.utils.extention.todo
@Composable @Composable
fun DetailScreen( fun DetailScreen(
@ -54,9 +57,6 @@ fun DetailScreen(
onClose = { onClose = {
bottomDetailState.collapse() bottomDetailState.collapse()
}, },
onMobi = {},
onEpub = {},
onSend = {},
) )
} }
} }
@ -67,10 +67,10 @@ fun DetailScreen(
private fun DetailScreenContent( private fun DetailScreenContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
book: BookUio, book: BookUio,
onClose: () -> Unit, onClose: () -> Unit = todo(),
onMobi: () -> Unit, onMobi: () -> Unit = todo(),
onEpub: () -> Unit, onEpub: () -> Unit = todo(),
onSend: () -> Unit, onSend: () -> Unit = todo(),
) { ) {
AnimatedDelayer(delay = Delay(300)) { AnimatedDelayer(delay = Delay(300)) {
Column( Column(
@ -291,12 +291,6 @@ private fun DetailScreenContentPreview() {
cover = cover, cover = cover,
) )
BibLibTheme { BibLibTheme {
DetailScreenContent( DetailScreenContent(book = book)
book = book,
onClose = {},
onMobi = {},
onEpub = {},
onSend = {},
)
} }
} }

View file

@ -1,115 +1,40 @@
package com.pixelized.biblib.ui.screen.home package com.pixelized.biblib.ui.screen.home
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material.* import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.hilt.navigation.compose.hiltViewModel
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.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.navigation.BottomDetailScaffold import com.pixelized.biblib.ui.navigation.BottomDetailScaffold
import com.pixelized.biblib.ui.navigation.page.Page
import com.pixelized.biblib.ui.navigation.page.PageNavHost import com.pixelized.biblib.ui.navigation.page.PageNavHost
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun HomeScreen() { fun HomeScreen(
val pageNavHostController = rememberNavController() accountViewModel: HomeViewModel = hiltViewModel(),
navHostController: NavHostController = rememberNavController(),
) {
BottomDetailScaffold { BottomDetailScaffold {
Scaffold( Scaffold(
modifier = Modifier.systemBarsPadding(), modifier = Modifier.systemBarsPadding(),
topBar = { topBar = {
Column { Column {
TopAppBar( HomeSearchBar()
title = { HomeTarBar(homePageNavController = navHostController)
Text(text = stringResource(id = R.string.app_name))
}
)
BottomBarNavigation(
homePageNavController = pageNavHostController
)
} }
}, },
content = { content = {
PageNavHost( PageNavHost(
modifier = Modifier.padding(it), modifier = Modifier.padding(it),
navHostController = pageNavHostController, navHostController = navHostController,
) )
} }
) )
} }
}
@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,
)
} }

View file

@ -0,0 +1,100 @@
package com.pixelized.biblib.ui.screen.home
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.biblib.R
import com.pixelized.biblib.ui.navigation.screen.LocalScreenNavHostController
import com.pixelized.biblib.ui.navigation.screen.navigateToProfile
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.utils.extention.todo
@Composable
fun HomeSearchBar(
accountViewModel: HomeViewModel = hiltViewModel()
) {
val screenNavHostController = LocalScreenNavHostController.current
val avatar by rememberUpdatedState(accountViewModel.avatar)
HomeSearchBarContent(
modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp),
avatar = avatar,
onAvatar = {
screenNavHostController.navigateToProfile()
}
)
}
@Composable
private fun HomeSearchBarContent(
modifier: Modifier = Modifier,
avatar: Painter,
onAvatar: () -> Unit = todo(),
onSearch: () -> Unit = todo(),
) {
Card(
modifier = modifier,
elevation = 8.dp
) {
Row(
modifier = Modifier
.clickable(onClick = onSearch)
.fillMaxWidth()
.height(48.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
modifier = Modifier
.size(42.dp)
.padding(horizontal = 8.dp),
imageVector = Icons.Default.Search,
contentDescription = null
)
Text(
modifier = Modifier.weight(1f),
color = MaterialTheme.colors.onSurface,
text = "Rechercher sur BibLib"
)
IconButton(
modifier = Modifier.padding(horizontal = 8.dp),
onClick = onAvatar,
) {
Image(
modifier = Modifier
.size(32.dp)
.clip(RoundedCornerShape(50)),
painter = avatar,
contentDescription = null
)
}
}
}
}
@Composable
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun HomeSearchBarContentPreview() {
BibLibTheme {
HomeSearchBarContent(
avatar = painterResource(R.drawable.ic_baseline_auto_stories_24)
)
}
}

View file

@ -0,0 +1,91 @@
package com.pixelized.biblib.ui.screen.home
import android.content.res.Configuration
import androidx.compose.foundation.layout.RowScope
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.compose.ui.tooling.preview.Preview
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.pixelized.biblib.ui.navigation.page.Page
import com.pixelized.biblib.ui.theme.BibLibTheme
@Composable
fun HomeTarBar(
homePageNavController: NavHostController = rememberNavController(),
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,
)
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun BottomBarNavigationPreview() {
BibLibTheme {
HomeTarBar()
}
}

View file

@ -0,0 +1,47 @@
package com.pixelized.biblib.ui.screen.home
import android.app.Application
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.layout.ContentScale
import com.pixelized.biblib.R
import com.pixelized.biblib.repository.googleSignIn.IGoogleSingInRepository
import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio
import com.pixelized.biblib.ui.screen.home.common.viewModel.ACoverViewModel
import com.pixelized.biblib.utils.cache.BookCoverCache
import com.pixelized.biblib.utils.extention.context
import com.pixelized.biblib.utils.painterResource
import dagger.hilt.android.lifecycle.HiltViewModel
import java.net.URL
import javax.inject.Inject
@HiltViewModel
class HomeViewModel @Inject constructor(
application: Application,
cacheBook: BookCoverCache,
private val account: IGoogleSingInRepository,
) : ACoverViewModel(application, cacheBook) {
private val _avatar = mutableStateOf(
painterResource(context, R.drawable.ic_launcher_foreground)
)
val avatar by _avatar
init {
updateAvatar()
}
private fun updateAvatar() {
account.account?.photoUrl?.let {
_avatar.value = cover(
placeHolder = CoverUio(
type = CoverUio.Type.PLACE_HOLDER,
painter = painterResource(context, R.drawable.ic_launcher_foreground),
),
type = CoverUio.Type.DETAIL,
contentScale = ContentScale.None,
url = URL("$it"),
).value.painter
}
}
}

View file

@ -1,8 +1,6 @@
package com.pixelized.biblib.ui.screen.home.common.uio package com.pixelized.biblib.ui.screen.home.common.uio
import androidx.compose.runtime.State import androidx.compose.runtime.State
import com.pixelized.biblib.network.client.IBibLibClient.Companion.COVER_URL
import java.net.URL
data class BookUio( data class BookUio(
val id: Int, val id: Int,

View file

@ -6,25 +6,25 @@ import androidx.compose.ui.layout.ContentScale
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio
import com.pixelized.biblib.utils.CoverCache import com.pixelized.biblib.utils.cache.BookCoverCache
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import java.net.URL import java.net.URL
abstract class ACoverViewModel( abstract class ACoverViewModel(
application: Application, application: Application,
private val cache: CoverCache, private val cacheBook: BookCoverCache,
) : AndroidViewModel(application) { ) : AndroidViewModel(application) {
fun cover( fun cover(
cache: CoverCache = this.cache, cacheBook: BookCoverCache = this.cacheBook,
coroutineScope: CoroutineScope = viewModelScope, coroutineScope: CoroutineScope = viewModelScope,
placeHolder: CoverUio, placeHolder: CoverUio,
type: CoverUio.Type, type: CoverUio.Type,
contentScale: ContentScale = ContentScale.FillBounds, contentScale: ContentScale = ContentScale.FillBounds,
tint: ColorFilter? = null, tint: ColorFilter? = null,
url: URL, url: URL,
) = cache.cover( ) = cacheBook.cover(
placeHolder = placeHolder, placeHolder = placeHolder,
coroutineScope = coroutineScope, coroutineScope = coroutineScope,
type = type, type = type,

View file

@ -13,7 +13,7 @@ import com.pixelized.biblib.repository.book.IBookRepository
import com.pixelized.biblib.ui.screen.home.common.uio.BookThumbnailUio import com.pixelized.biblib.ui.screen.home.common.uio.BookThumbnailUio
import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio
import com.pixelized.biblib.ui.screen.home.common.viewModel.ACoverViewModel import com.pixelized.biblib.ui.screen.home.common.viewModel.ACoverViewModel
import com.pixelized.biblib.utils.CoverCache import com.pixelized.biblib.utils.cache.BookCoverCache
import com.pixelized.biblib.utils.extention.context import com.pixelized.biblib.utils.extention.context
import com.pixelized.biblib.utils.extention.longDate import com.pixelized.biblib.utils.extention.longDate
import com.pixelized.biblib.utils.painterResource import com.pixelized.biblib.utils.painterResource
@ -27,8 +27,8 @@ import javax.inject.Inject
class BooksViewModel @Inject constructor( class BooksViewModel @Inject constructor(
application: Application, application: Application,
bookRepository: IBookRepository, bookRepository: IBookRepository,
cache: CoverCache, cacheBook: BookCoverCache,
) : ACoverViewModel(application, cache) { ) : ACoverViewModel(application, cacheBook) {
private val booksSource = Pager( private val booksSource = Pager(
config = PagingConfig(pageSize = PAGING_SIZE), config = PagingConfig(pageSize = PAGING_SIZE),

View file

@ -14,7 +14,7 @@ import com.pixelized.biblib.repository.book.IBookRepository
import com.pixelized.biblib.ui.screen.home.common.uio.BookThumbnailUio import com.pixelized.biblib.ui.screen.home.common.uio.BookThumbnailUio
import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio
import com.pixelized.biblib.ui.screen.home.common.viewModel.ACoverViewModel import com.pixelized.biblib.ui.screen.home.common.viewModel.ACoverViewModel
import com.pixelized.biblib.utils.CoverCache import com.pixelized.biblib.utils.cache.BookCoverCache
import com.pixelized.biblib.utils.extention.context import com.pixelized.biblib.utils.extention.context
import com.pixelized.biblib.utils.extention.longDate import com.pixelized.biblib.utils.extention.longDate
import com.pixelized.biblib.utils.painterResource import com.pixelized.biblib.utils.painterResource
@ -29,8 +29,8 @@ import javax.inject.Inject
class NewsBookViewModel @Inject constructor( class NewsBookViewModel @Inject constructor(
application: Application, application: Application,
bookRepository: IBookRepository, bookRepository: IBookRepository,
cache: CoverCache, cacheBook: BookCoverCache,
) : ACoverViewModel(application, cache) { ) : ACoverViewModel(application, cacheBook) {
private val newsSource: Flow<PagingData<BookThumbnailUio>> = Pager( private val newsSource: Flow<PagingData<BookThumbnailUio>> = Pager(
config = PagingConfig(pageSize = PAGING_SIZE), config = PagingConfig(pageSize = PAGING_SIZE),

View file

@ -42,7 +42,7 @@ class LauncherViewModel @Inject constructor(
loadAllBooks() loadAllBooks()
} }
} }
// // Update loading state.
_isLoading.value = false _isLoading.value = false
} }
} }

View file

@ -0,0 +1,92 @@
package com.pixelized.biblib.ui.screen.profile
import android.content.Intent
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.net.Uri
import androidx.compose.foundation.layout.Column
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.ui.composable.StateUio
import com.pixelized.biblib.ui.theme.BibLibTheme
@Composable
fun ProfileScreen(
viewModel: ProfileViewModel = hiltViewModel(),
) {
val context = LocalContext.current
val user by rememberUpdatedState(newValue = viewModel.user)
when (val unwrap = user) {
is StateUio.Success -> ProfileScreenContent(user = unwrap.value)
}
FloatingActionButton(
onClick = {
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(IBibLibClient.EDIT_PROFILE)))
},
content = { },
)
}
@Composable
private fun ProfileScreenContent(
modifier: Modifier = Modifier,
user: ProfileViewModel.UserUio,
) {
Column(modifier = modifier) {
Text(
style = MaterialTheme.typography.h5,
color = MaterialTheme.colors.onSurface,
text = user.username,
)
user.firstname?.let {
Text(
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onSurface,
text = it,
)
}
user.lastname?.let {
Text(
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onSurface,
text = it,
)
}
user.amazonEmails.forEach {
Text(
style = MaterialTheme.typography.body1,
color = MaterialTheme.colors.onSurface,
text = it,
)
}
}
}
@Composable
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO)
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
private fun ProfileScreenContentPreview() {
BibLibTheme {
val user = ProfileViewModel.UserUio(
username = "R. Daneel Olivaw",
firstname = "R. Daneel",
lastname = "Olivaw",
amazonEmails = listOf("r.daneel.olivaw@biblib.com"),
)
ProfileScreenContent(
user = user
)
}
}

View file

@ -0,0 +1,60 @@
package com.pixelized.biblib.ui.screen.profile
import android.app.Application
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.viewModelScope
import com.pixelized.biblib.model.user.User
import com.pixelized.biblib.network.client.IBibLibClient
import com.pixelized.biblib.network.factory.UserFactory
import com.pixelized.biblib.ui.composable.StateUio
import com.pixelized.biblib.ui.screen.home.common.viewModel.ACoverViewModel
import com.pixelized.biblib.utils.cache.BookCoverCache
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class ProfileViewModel @Inject constructor(
application: Application,
cacheBook: BookCoverCache,
private val client: IBibLibClient,
) : ACoverViewModel(application, cacheBook) {
private val _user = mutableStateOf<StateUio<UserUio>>(StateUio.Progress())
val user by _user
init {
updateUser()
}
private fun updateUser() {
viewModelScope.launch(Dispatchers.IO) {
try {
val factory = UserFactory()
val response = client.service.user()
val user = factory.fromUserResponseToUser(response)
_user.value = StateUio.Success(user.toUio())
} catch (exception: Exception) {
Log.e("AccountViewModel", exception.message, exception)
_user.value = StateUio.Failure(exception)
}
}
}
data class UserUio(
val username: String,
val firstname: String?,
val lastname: String?,
val amazonEmails: List<String>,
)
}
private fun User.toUio() = ProfileViewModel.UserUio(
username = username,
firstname = firstname,
lastname = lastname,
amazonEmails = amazonEmails.toList(),
)

View file

@ -1,7 +1,5 @@
package com.pixelized.biblib.utils package com.pixelized.biblib.utils.cache
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.util.Log import android.util.Log
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
@ -14,16 +12,13 @@ import androidx.compose.ui.layout.ContentScale
import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio import com.pixelized.biblib.ui.screen.home.common.uio.CoverUio
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File
import java.io.IOException
import java.net.URL import java.net.URL
import javax.inject.Inject import javax.inject.Inject
class CoverCache @Inject constructor(context: Context) { class BookCoverCache @Inject constructor(
private var cache: File? = context.cacheDir private val cache: ImageCache
) {
fun cover( fun cover(
coroutineScope: CoroutineScope, coroutineScope: CoroutineScope,
placeHolder: CoverUio, placeHolder: CoverUio,
@ -33,7 +28,7 @@ class CoverCache @Inject constructor(context: Context) {
tint: ColorFilter?, tint: ColorFilter?,
): State<CoverUio> { ): State<CoverUio> {
// read the cache a convert it to a UIO. // read the cache a convert it to a UIO.
val cache = readFromDisk(url)?.let { val cache = cache.readFromDisk(url)?.let {
CoverUio( CoverUio(
type = type, type = type,
contentScale = contentScale, contentScale = contentScale,
@ -65,7 +60,7 @@ class CoverCache @Inject constructor(context: Context) {
try { try {
val bitmap = BitmapFactory.decodeStream(url.openStream()) val bitmap = BitmapFactory.decodeStream(url.openStream())
val painter = BitmapPainter(bitmap.asImageBitmap()) val painter = BitmapPainter(bitmap.asImageBitmap())
writeToDisk(url, bitmap) cache.writeToDisk(url, bitmap)
value = CoverUio( value = CoverUio(
type = type, type = type,
contentScale = contentScale, contentScale = contentScale,
@ -76,26 +71,4 @@ class CoverCache @Inject constructor(context: Context) {
Log.w("CoverCache", "Fail to download: {$url}", exception) Log.w("CoverCache", "Fail to download: {$url}", exception)
} }
} }
private fun file(url: URL): File = File(cache?.absolutePath + url.file)
private fun writeToDisk(url: URL, bitmap: Bitmap) {
val file = file(url)
try {
file.mkdirs()
file.delete()
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, file.outputStream())
} catch (e: Exception) {
Log.e("BitmapCache", "bitmap?.compress() FAILED !", e)
}
}
private fun readFromDisk(url: URL): Bitmap? {
val file = file(url)
return try {
if (file.exists()) BitmapFactory.decodeStream(file.inputStream()) else null
} catch (e: IOException) {
null
}
}
} }

View file

@ -0,0 +1,37 @@
package com.pixelized.biblib.utils.cache
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.util.Log
import java.io.File
import java.io.IOException
import java.net.URL
import javax.inject.Inject
class ImageCache @Inject constructor(context: Context) {
private var cache: File? = context.cacheDir
private fun file(url: URL): File = File(cache?.absolutePath + url.file)
fun writeToDisk(url: URL, bitmap: Bitmap) {
val file = file(url)
try {
file.mkdirs()
file.delete()
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, file.outputStream())
} catch (e: Exception) {
Log.e("BitmapCache", "bitmap?.compress() FAILED !", e)
}
}
fun readFromDisk(url: URL): Bitmap? {
val file = file(url)
return try {
if (file.exists()) BitmapFactory.decodeStream(file.inputStream()) else null
} catch (e: IOException) {
null
}
}
}

View file

@ -2,6 +2,18 @@ package com.pixelized.biblib.utils.extention
import android.content.Context import android.content.Context
import android.widget.Toast import android.widget.Toast
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
fun Context.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) = fun Context.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) =
Toast.makeText(this, message, duration).show() Toast.makeText(this, message, duration).show()
fun Context.notImplemented() {
showToast("Not implemented yet")
}
@Composable
fun todo(): () -> Unit {
val context = LocalContext.current
return { context.notImplemented() }
}

View file

@ -12,5 +12,6 @@ class UserMock {
firstname = "R. Daneel", firstname = "R. Daneel",
lastname = "Olivaw", lastname = "Olivaw",
amazonEmails = listOf("r.daneel.olivaw@biblib.com"), amazonEmails = listOf("r.daneel.olivaw@biblib.com"),
downloadedBooks = listOf(),
) )
} }