add user data fetch support.

This commit is contained in:
Thomas Andres Gomez 2021-11-27 14:28:33 +01:00
parent 9ce20ba374
commit 6254ec4f02
28 changed files with 467 additions and 142 deletions

View file

@ -17,8 +17,10 @@ import com.pixelized.biblib.repository.credential.CredentialRepository
import com.pixelized.biblib.repository.credential.ICredentialRepository
import com.pixelized.biblib.repository.googleSignIn.GoogleSingInRepository
import com.pixelized.biblib.repository.googleSignIn.IGoogleSingInRepository
import com.pixelized.biblib.repository.user.IUserRepository
import com.pixelized.biblib.repository.user.UserRepository
import com.pixelized.biblib.utils.BitmapCache
import com.pixelized.biblib.utils.injection.Bob
import com.pixelized.biblib.utils.injection.ServiceLocator
class BibLibApplication : Application() {
@ -26,21 +28,22 @@ class BibLibApplication : Application() {
super.onCreate()
// Android.
Bob[BibLibDatabase::class] =
ServiceLocator[BibLibDatabase::class] =
Room.databaseBuilder(this, BibLibDatabase::class.java, BibLibDatabase.DATABASE_NAME)
.fallbackToDestructiveMigration()
.build()
Bob[SharedPreferences::class] = getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE)
ServiceLocator[SharedPreferences::class] = getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE)
// Web service.
Bob[Gson::class] = GsonBuilder().create()
Bob[IBibLibClient::class] = BibLibClient()
ServiceLocator[Gson::class] = GsonBuilder().create()
ServiceLocator[IBibLibClient::class] = BibLibClient()
// Bitmap cache.
Bob[BitmapCache::class] = BitmapCache(this)
ServiceLocator[BitmapCache::class] = BitmapCache(this)
// Repositories.
Bob[IGoogleSingInRepository::class] = GoogleSingInRepository(this)
Bob[ICredentialRepository::class] = CredentialRepository()
Bob[IAPICacheRepository::class] = APICacheRepository()
Bob[IBookRepository::class] = BookRepository()
ServiceLocator[IGoogleSingInRepository::class] = GoogleSingInRepository(this)
ServiceLocator[ICredentialRepository::class] = CredentialRepository()
ServiceLocator[IAPICacheRepository::class] = APICacheRepository()
ServiceLocator[IBookRepository::class] = BookRepository()
ServiceLocator[IUserRepository::class] = UserRepository()
}
companion object {

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.model
package com.pixelized.biblib.model.book
data class Author(
val id: String,

View file

@ -1,6 +1,6 @@
package com.pixelized.biblib.model
package com.pixelized.biblib.model.book
import java.util.*
import java.util.Date
data class Book(
val id: Int,

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.model
package com.pixelized.biblib.model.book
data class Genre(
val id: String,

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.model
package com.pixelized.biblib.model.book
import java.util.*

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.model
package com.pixelized.biblib.model.book
data class Series(
val id: String?,

View file

@ -0,0 +1,13 @@
package com.pixelized.biblib.model.user
import java.util.Date
data class User(
val id: String,
val created: Date,
val updated: Date,
val username: String,
val firstname: String?,
val lastname: String?,
val amazonEmails: List<String>,
)

View file

@ -1,9 +1,11 @@
package com.pixelized.biblib.network.client
import com.google.gson.internal.LinkedTreeMap
import com.pixelized.biblib.network.data.query.AuthLoginQuery
import com.pixelized.biblib.network.data.response.AuthLoginResponse
import com.pixelized.biblib.network.data.response.BookDetailResponse
import com.pixelized.biblib.network.data.response.BookListResponse
import com.pixelized.biblib.network.data.response.UserResponse
import retrofit2.http.*
interface IBibLibWebServiceAPI {
@ -13,8 +15,8 @@ interface IBibLibWebServiceAPI {
@GET("/authent/google-id-token")
suspend fun loginWithGoogle(@Query("id_token") token: String): AuthLoginResponse
// @GET("/authent/user")
// suspend fun user(): UserResponse
@GET("/authent/user")
suspend fun user(): UserResponse
@GET("/api/book/new")
suspend fun new(): BookListResponse
@ -25,9 +27,9 @@ interface IBibLibWebServiceAPI {
@GET("/api/book/{id}")
suspend fun detail(@Path("id") bookId: Int): BookDetailResponse
// @GET("/api/book/{id}/send/kindle")
// suspend fun send(@Path("id") bookId: Int, @Query("mail") mail: String): LinkedTreeMap<String, Any>
//
@GET("/api/book/{id}/send/kindle")
suspend fun send(@Path("id") bookId: Int, @Query("mail") mail: String): LinkedTreeMap<String, Any>
// @GET("/api/book/{id}/epub/url")
// suspend fun epub(@Path("id") bookId: Int): TokenResponse
//

View file

@ -0,0 +1,109 @@
package com.pixelized.biblib.network.data.response
import com.google.gson.annotations.SerializedName
data class UserResponse(
@SerializedName("data")
val data: Data? = null,
) : ErrorResponse() {
data class Data(
@SerializedName("id")
val id: String? = null,
@SerializedName("created")
val created: String? = null,
@SerializedName("updated")
val updated: String? = null,
@SerializedName("local")
val local: Local? = null,
@SerializedName("facebook")
val facebook: Facebook? = null,
@SerializedName("twitter")
val twitter: Twitter? = null,
@SerializedName("google")
val google: Google? = null,
@SerializedName("history")
val history: History? = null,
)
data class Local(
@SerializedName("username")
val username: String? = null,
@SerializedName("firstname")
val firstname: String? = null,
@SerializedName("lastname")
val lastname: String? = null,
@SerializedName("email")
val email: String? = null,
@SerializedName("isAdmin")
val isAdmin: String? = null,
@SerializedName("amazonEmails")
val amazonEmails: List<String>? = null,
)
data class Facebook(
@SerializedName("id")
val id: String? = null,
@SerializedName("email")
val email: String? = null,
@SerializedName("name")
val name: String? = null,
)
data class Twitter(
@SerializedName("id")
val id: String? = null,
@SerializedName("email")
val email: String? = null,
@SerializedName("name")
val name: String? = null,
)
data class Google(
@SerializedName("id")
val id: String? = null,
@SerializedName("email")
val email: String? = null,
@SerializedName("name")
val name: String? = null,
)
data class History(
@SerializedName("lastConnection")
val lastConnection: String? = null,
@SerializedName("downloadedBooks")
val downloadedBooks: List<EBook>? = null,
@SerializedName("ratings")
val ratings: List<Rating>? = null,
)
data class EBook(
@SerializedName("id")
val id: Int? = null,
@SerializedName("date")
val date: String? = null,
@SerializedName("data")
val data: MetaEBook? = null,
)
data class MetaEBook(
@SerializedName("data_id")
val data_id: String? = null,
@SerializedName("data_format")
val data_format: String? = null,
@SerializedName("data_size")
val data_size: String? = null,
@SerializedName("data_name")
val data_name: String? = null,
)
data class Rating(
@SerializedName("book_id")
val book_id: Int? = null,
@SerializedName("rating")
val rating: Float? = null,
@SerializedName("book_name")
val book_name: String? = null,
@SerializedName("date")
val date: String? = null,
)
}

View file

@ -1,6 +1,6 @@
package com.pixelized.biblib.network.factory
import com.pixelized.biblib.model.*
import com.pixelized.biblib.model.book.*
import com.pixelized.biblib.network.data.response.BookDetailResponse
import com.pixelized.biblib.network.data.response.BookListResponse
import com.pixelized.biblib.utils.exception.MandatoryFieldMissingException
@ -9,7 +9,7 @@ import java.text.SimpleDateFormat
import java.util.*
class BookFactory {
private val parser get() = SimpleDateFormat(FORMAT, Locale.getDefault())
private val parser get() = SimpleDateFormat(Factory.FORMAT, Locale.getDefault())
fun fromListResponseToBook(
response: BookListResponse.Book,
@ -160,8 +160,4 @@ class BookFactory {
isNew = isNew
)
}
companion object {
private const val FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
}
}

View file

@ -0,0 +1,5 @@
package com.pixelized.biblib.network.factory
object Factory {
const val FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
}

View file

@ -0,0 +1,37 @@
package com.pixelized.biblib.network.factory
import com.pixelized.biblib.model.user.User
import com.pixelized.biblib.network.data.response.UserResponse
import com.pixelized.biblib.utils.exception.MandatoryFieldMissingException
import java.text.SimpleDateFormat
import java.util.*
class UserFactory {
private val parser get() = SimpleDateFormat(Factory.FORMAT, Locale.getDefault())
fun fromUserResponseToUser(
response: UserResponse
): User {
fun error(name: String) =
MandatoryFieldMissingException("#fromUserResponseToUser()", name, response)
val id = response.data?.id
val created = response.data?.created?.let { parser.parse(it) }
val updated = response.data?.updated?.let { parser.parse(it) }
val username = response.data?.local?.username
val firstname = response.data?.local?.firstname
val lastname = response.data?.local?.lastname
val amazonEmails = response.data?.local?.amazonEmails
return User(
id = id ?: throw error("id"),
created = created ?: throw error("created"),
updated = updated ?: throw error("updated"),
username = username ?: throw error("username"),
firstname = firstname,
lastname = lastname,
amazonEmails = amazonEmails ?: listOf(),
)
}
}

View file

@ -7,7 +7,7 @@ import com.pixelized.biblib.database.crossref.BookAuthorCrossRef
import com.pixelized.biblib.database.crossref.BookGenreCrossRef
import com.pixelized.biblib.database.data.*
import com.pixelized.biblib.database.relation.BookRelation
import com.pixelized.biblib.model.*
import com.pixelized.biblib.model.book.*
import com.pixelized.biblib.utils.injection.inject
class BookRepository : IBookRepository {

View file

@ -1,7 +1,7 @@
package com.pixelized.biblib.repository.book
import androidx.paging.DataSource
import com.pixelized.biblib.model.Book
import com.pixelized.biblib.model.book.Book
interface IBookRepository {

View file

@ -0,0 +1,8 @@
package com.pixelized.biblib.repository.user
import com.pixelized.biblib.model.user.User
interface IUserRepository {
suspend fun getUser(forceUpdate: Boolean = false): User
suspend fun amazonEmails(): List<String>
}

View file

@ -0,0 +1,22 @@
package com.pixelized.biblib.repository.user
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.utils.injection.inject
class UserRepository : IUserRepository {
private val client: IBibLibClient by inject()
private var user: User? = null
override suspend fun getUser(forceUpdate: Boolean): User {
return client.service.user().let { response ->
val factory = UserFactory()
factory.fromUserResponseToUser(response).also { user = it }
}
}
override suspend fun amazonEmails(): List<String> {
return user?.amazonEmails ?: listOf()
}
}

View file

@ -10,6 +10,8 @@ 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.runtime.State
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
@ -18,12 +20,15 @@ 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.model.user.User
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.viewmodel.user.IUserViewModel
import java.util.*
@Composable
fun BibLibDrawer(
userViewModel : IUserViewModel,
onNewClick: () -> Unit = {},
onBookClick: () -> Unit = {},
onSeriesClick: () -> Unit = {},
@ -45,13 +50,14 @@ fun BibLibDrawer(
.fillMaxWidth()
.padding(16.dp)
) {
val user: State<User?> = userViewModel.user.observeAsState()
Text(
style = typography.body1,
text = "R. Daneel Olivaw"
text = user.value?.username ?: ""
)
Text(
style = typography.caption,
text = "r.daneel.olivaw@biblib.com"
text = user.value?.amazonEmails?.firstOrNull() ?: ""
)
}
}
@ -126,6 +132,8 @@ private fun DrawerItem(text: String, imageVector: ImageVector, onClick: () -> Un
@Composable
fun BibLibDrawerLightPreview() {
BibLibTheme {
BibLibDrawer()
BibLibDrawer(
userViewModel = IUserViewModel.Mock()
)
}
}

View file

@ -23,7 +23,7 @@ import com.pixelized.biblib.ui.data.BookThumbnailUio
import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.theme.Teal200
import com.pixelized.biblib.utils.BitmapCache
import com.pixelized.biblib.utils.injection.Bob
import com.pixelized.biblib.utils.injection.ServiceLocator
import com.pixelized.biblib.utils.mock.BookThumbnailMock
private val THUMBNAIL_WIDTH: Dp = 60.dp
@ -157,7 +157,7 @@ private fun Placeholder(modifier: Modifier) = Surface(modifier = modifier, eleva
@Preview
@Composable
fun BookItemLightPreview() {
Bob[BitmapCache::class] = BitmapCache(LocalContext.current)
ServiceLocator[BitmapCache::class] = BitmapCache(LocalContext.current)
BibLibTheme {
val mock = BookThumbnailMock()
FilledBookItem(thumbnail = mock.bookThumbnail)
@ -167,7 +167,7 @@ fun BookItemLightPreview() {
@Preview
@Composable
fun BookItemDarkPreview() {
Bob[BitmapCache::class] = BitmapCache(LocalContext.current)
ServiceLocator[BitmapCache::class] = BitmapCache(LocalContext.current)
BibLibTheme(darkTheme = true) {
val mock = BookThumbnailMock()
FilledBookItem(thumbnail = mock.bookThumbnail)

View file

@ -1,7 +1,12 @@
package com.pixelized.biblib.ui.composable.pages
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
@ -12,6 +17,7 @@ import androidx.compose.material.icons.filled.Send
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -34,14 +40,17 @@ import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.theme.Teal200
import com.pixelized.biblib.ui.viewmodel.book.BooksViewModel
import com.pixelized.biblib.ui.viewmodel.book.IBooksViewModel
import com.pixelized.biblib.ui.viewmodel.user.IUserViewModel
import com.pixelized.biblib.ui.viewmodel.user.UserViewModel
import com.pixelized.biblib.utils.BitmapCache
import com.pixelized.biblib.utils.injection.Bob
import com.pixelized.biblib.utils.injection.ServiceLocator
import com.pixelized.biblib.utils.mock.BookMock
@Composable
fun DetailPage(
booksViewModel: IBooksViewModel = viewModel<BooksViewModel>(),
userViewModel: IUserViewModel = viewModel<UserViewModel>(),
bookId: Int
) {
Surface(
@ -51,15 +60,41 @@ fun DetailPage(
elevation = 4.dp
) {
val book by booksViewModel.getBookDetail(bookId).observeAsState()
val user by userViewModel.user.observeAsState()
book?.let {
DetailPage(book = it)
DetailPage(
book = it,
onSendClick = {
val emailCount = user?.amazonEmails?.size ?: 0
when {
emailCount == 1 -> {
val email: String? = user?.amazonEmails?.firstOrNull()
if (email != null) {
booksViewModel.send(id = bookId, mail = email)
}
}
emailCount > 1 -> {
// TODO choose email popup
}
else -> Unit // TODO warning popup, no email.
}
}
)
}
}
}
@Composable
fun DetailPage(book: BookUio) {
@OptIn(ExperimentalAnimationApi::class)
fun DetailPage(
book: BookUio,
onEpubClick: () -> Unit = {},
onMobiClick: () -> Unit = {},
onSendClick: () -> Unit = {},
) {
val typography = MaterialTheme.typography
val pouet = remember { MutableTransitionState(false) }
Column(
modifier = Modifier
@ -86,18 +121,21 @@ fun DetailPage(book: BookUio) {
Button(
modifier = Modifier.padding(end = 4.dp),
imageVector = Icons.Default.Download,
text = stringResource(id = R.string.action_epub)
) { }
text = stringResource(id = R.string.action_epub),
onClick = onEpubClick,
)
Button(
modifier = Modifier.padding(horizontal = 4.dp),
imageVector = Icons.Default.Download,
text = stringResource(id = R.string.action_mobi),
) { }
onClick = onMobiClick,
)
Button(
modifier = Modifier.padding(start = 4.dp),
imageVector = Icons.Default.Send,
text = stringResource(id = R.string.action_send),
) { }
onClick = onSendClick,
)
}
Text(
modifier = Modifier
@ -141,6 +179,13 @@ fun DetailPage(book: BookUio) {
modifier = Modifier.padding(bottom = 16.dp)
)
}
AnimatedVisibility(
visibleState = pouet,
) {
SendMailBottomSheet()
}
}
@Composable
@ -183,12 +228,30 @@ private fun RowScope.Button(
}
}
@Composable
private fun SendMailBottomSheet() {
val data = listOf("pouet", "pouet")
LazyColumn(modifier = Modifier.fillMaxWidth()) {
items(data.size) { index: Int ->
Text(text = data[index])
}
}
}
@Preview
@Composable
fun DetailPageLightPreview() {
Bob[BitmapCache::class] = BitmapCache(LocalContext.current)
ServiceLocator[BitmapCache::class] = BitmapCache(LocalContext.current)
BibLibTheme {
val mock = BookMock()
DetailPage(mock.book)
val book = BookMock()
DetailPage(book.book)
}
}
@Preview
@Composable
fun SendMailBottomSheetPreview() {
BibLibTheme {
SendMailBottomSheet()
}
}

View file

@ -1,10 +1,7 @@
package com.pixelized.biblib.ui.composable.screen
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.*
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.*
@ -22,6 +19,8 @@ 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 com.pixelized.biblib.ui.viewmodel.user.IUserViewModel
import com.pixelized.biblib.ui.viewmodel.user.UserViewModel
import kotlinx.coroutines.launch
@ -30,6 +29,7 @@ import kotlinx.coroutines.launch
fun HomeScreen(
navigationViewModel: INavigationViewModel = viewModel<NavigationViewModel>(),
booksViewModel: IBooksViewModel = viewModel<BooksViewModel>(),
userViewModel: IUserViewModel = viewModel<UserViewModel>()
) {
// navigation
val page by navigationViewModel.page.observeAsState()
@ -59,6 +59,7 @@ fun HomeScreen(
},
drawerContent = {
BibLibDrawer(
userViewModel = userViewModel,
onNewClick = {
coroutineScope.launch { scaffoldState.drawerState.close() }
},
@ -85,20 +86,19 @@ fun HomeScreen(
AnimatedVisibility(
visible = page is Page.Detail,
initiallyVisible = false,
enter = slideInVertically(
enter = slideInHorizontally(
animationSpec = tween(Animation.MEDIUM_DURATION),
initialOffsetY = { height -> height },
initialOffsetX = { width -> width },
),
exit = slideOutVertically(
exit = slideOutHorizontally(
animationSpec = tween(Animation.MEDIUM_DURATION),
targetOffsetY = { height -> height },
targetOffsetX = { width -> width },
),
) {
// 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) }
currentPage?.let { DetailPage(booksViewModel, userViewModel, it.bookId) }
}
}
}
@ -109,7 +109,8 @@ fun MainScreenComposablePreview() {
BibLibTheme {
HomeScreen(
INavigationViewModel.Mock(page = Page.Home.New),
IBooksViewModel.Mock()
IBooksViewModel.Mock(),
IUserViewModel.Mock()
)
}
}

View file

@ -3,10 +3,7 @@ package com.pixelized.biblib.ui.composable.screen
import android.content.Intent
import android.net.Uri
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -35,10 +32,11 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
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.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
@ -110,85 +108,37 @@ private fun Dialogs(
val authState by authenticationViewModel.state.observeAsState()
val bookState by booksViewModel.state.observeAsState()
val visible = authState !is IAuthenticationViewModel.State.Initial
|| bookState !is IBooksViewModel.State.Initial
CrossFadeOverlay(
modifier = Modifier.clickable {
if (visible) {
authenticationViewModel.clearState()
if (authState !is IAuthenticationViewModel.State.Initial) {
val isInError = authState is IAuthenticationViewModel.State.Error
Dialog(
onDismissRequest = { authenticationViewModel.clearState() },
properties = DialogProperties(
dismissOnBackPress = isInError,
dismissOnClickOutside = isInError
)
) {
when {
bookState is IBooksViewModel.State.Error -> ErrorCard(
message = stringResource(id = R.string.error_book),
exception = (bookState as? IBooksViewModel.State.Error)?.exception
)
bookState is IBooksViewModel.State.Loading -> LoadingCard(
message = stringResource(id = R.string.loading_book)
)
bookState is IBooksViewModel.State.Finished -> SuccessCard(
message = stringResource(id = R.string.success_book)
)
authState is IAuthenticationViewModel.State.Error -> ErrorCard(
message = stringResource(id = R.string.error_authentication),
exception = (authState as? IAuthenticationViewModel.State.Error)?.exception
)
authState is IAuthenticationViewModel.State.Loading -> LoadingCard(
message = stringResource(id = R.string.loading_authentication)
)
authState is IAuthenticationViewModel.State.Finished -> SuccessCard(
message = stringResource(id = R.string.success_authentication)
)
}
},
visible = visible
) {
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = authState is IAuthenticationViewModel.State.Error,
initiallyVisible = false,
enter = expandVertically(Alignment.CenterVertically),
exit = shrinkVertically(Alignment.CenterVertically),
) {
ErrorCard(
message = stringResource(id = R.string.error_authentication),
exception = (authState as? IAuthenticationViewModel.State.Error)?.exception
)
}
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = authState is IAuthenticationViewModel.State.Loading,
initiallyVisible = false,
enter = expandVertically(Alignment.CenterVertically),
exit = shrinkVertically(Alignment.CenterVertically),
) {
LoadingCard(
message = stringResource(id = R.string.loading_authentication)
)
}
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = (authState as? IAuthenticationViewModel.State.Finished)?.logged
?: false,
initiallyVisible = false,
enter = expandVertically(Alignment.CenterVertically),
exit = shrinkVertically(Alignment.CenterVertically),
) {
SuccessCard(
message = stringResource(id = R.string.success_authentication)
)
}
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = bookState is IBooksViewModel.State.Error,
initiallyVisible = false,
enter = expandVertically(Alignment.CenterVertically),
exit = shrinkVertically(Alignment.CenterVertically),
) {
ErrorCard(
message = stringResource(id = R.string.error_book),
exception = (bookState as? IBooksViewModel.State.Error)?.exception
)
}
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = bookState is IBooksViewModel.State.Loading,
initiallyVisible = false,
enter = expandVertically(Alignment.CenterVertically),
exit = shrinkVertically(Alignment.CenterVertically),
) {
LoadingCard(
message = stringResource(id = R.string.loading_book)
)
}
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = bookState is IBooksViewModel.State.Finished,
initiallyVisible = false,
enter = expandVertically(Alignment.CenterVertically),
exit = shrinkVertically(Alignment.CenterVertically),
) {
SuccessCard(
message = stringResource(id = R.string.success_book)
)
}
}
}

View file

@ -28,12 +28,15 @@ 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.Screen
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel
import com.pixelized.biblib.ui.viewmodel.user.IUserViewModel
import com.pixelized.biblib.ui.viewmodel.user.UserViewModel
import kotlinx.coroutines.delay
import java.util.*
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_USER = "LAUNCH_EFFECT_USER"
@OptIn(ExperimentalAnimationApi::class)
@ -41,11 +44,13 @@ private const val LAUNCH_EFFECT_BOOK = "LAUNCH_EFFECT_BOOK"
fun SplashScreen(
authenticationViewModel: IAuthenticationViewModel = viewModel<AuthenticationViewModel>(),
bookViewModel: IBooksViewModel = viewModel<BooksViewModel>(),
userViewModel: IUserViewModel = viewModel<UserViewModel>(),
navigationViewModel: INavigationViewModel = viewModel<NavigationViewModel>(),
initiallyVisible: Boolean = false,
) {
val authenticationState: IAuthenticationViewModel.State? by authenticationViewModel.state.observeAsState()
val bookState by bookViewModel.state.observeAsState()
val userState by userViewModel.state.observeAsState()
val contentVisibility = remember { mutableStateOf(false) }
@ -73,6 +78,11 @@ fun SplashScreen(
bookViewModel.updateBooks()
}
(bookState as? IBooksViewModel.State.Finished)?.let {
LaunchedEffect(LAUNCH_EFFECT_USER) {
userViewModel.updateUser()
}
}
(userState as? IUserViewModel.State.Finished)?.let {
HandleExitAnimation(contentVisibility) {
navigationViewModel.navigateTo(Screen.MainScreen)
}

View file

@ -9,7 +9,7 @@ import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.PagingSource
import com.pixelized.biblib.model.Book
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
@ -23,6 +23,10 @@ import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
/**
* TODO: there is some book related code that should be inside a Repository // DataSource.
*/
class BooksViewModel : ViewModel(), IBooksViewModel {
private val bookRepository: IBookRepository by inject()
private val client: IBibLibClient by inject()
@ -79,6 +83,10 @@ class BooksViewModel : ViewModel(), IBooksViewModel {
return data
}
override fun send(id: Int, mail: String) {
// TODO()
}
private suspend fun loadNewBooks(): Boolean {
val cached = apiCache.new
val updated = client.service.new()

View file

@ -19,6 +19,8 @@ interface IBooksViewModel {
fun getBookDetail(id: Int): LiveData<BookUio>
fun send(id: Int, mail:String)
sealed class State {
object Initial : State()
object Loading : State()
@ -37,5 +39,6 @@ interface IBooksViewModel {
override fun updateBooks() = Unit
override fun getBookDetail(id: Int) = MutableLiveData<BookUio>()
override fun send(id: Int, mail: String) = Unit
}
}

View file

@ -0,0 +1,30 @@
package com.pixelized.biblib.ui.viewmodel.user
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.pixelized.biblib.R
import com.pixelized.biblib.model.user.User
import com.pixelized.biblib.ui.viewmodel.credential.ICredentialViewModel
import com.pixelized.biblib.utils.mock.UserMock
import java.util.*
interface IUserViewModel {
val state: LiveData<State>
val user: LiveData<User>
fun updateUser()
sealed class State {
object Initial : State()
object Loading : State()
object Finished : State()
data class Error(val exception: Exception) : State()
}
class Mock : IUserViewModel {
override val state: LiveData<State> = MutableLiveData()
override val user: LiveData<User> = MutableLiveData(UserMock().user)
override fun updateUser() = Unit
}
}

View file

@ -0,0 +1,41 @@
package com.pixelized.biblib.ui.viewmodel.user
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.biblib.model.user.User
import com.pixelized.biblib.repository.user.IUserRepository
import com.pixelized.biblib.ui.viewmodel.user.IUserViewModel.State
import com.pixelized.biblib.utils.injection.inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class UserViewModel : ViewModel(), IUserViewModel {
private val userRepository: IUserRepository by inject()
private val _state = MutableLiveData<State>()
override val state: LiveData<State> get() = _state
override val user: LiveData<User>
get() = MutableLiveData<User>().apply {
viewModelScope.launch(Dispatchers.IO) {
val user = userRepository.getUser(false)
postValue(user)
}
}
override fun updateUser() {
viewModelScope.launch(Dispatchers.IO) {
try {
_state.postValue(State.Initial)
_state.postValue(State.Loading)
userRepository.getUser(true)
_state.postValue(State.Finished)
} catch (e: Exception) {
_state.postValue(State.Error(e))
}
}
}
}

View file

@ -4,7 +4,7 @@ import com.pixelized.biblib.utils.exception.InjectionException
import kotlin.reflect.KClass
@Suppress("UNCHECKED_CAST")
object Bob {
object ServiceLocator {
private val components = hashMapOf<KClass<*>, Any>()
operator fun <I : Any, O : I> set(clazz: KClass<I>, component: O) {
@ -16,6 +16,6 @@ object Bob {
}
}
inline fun <reified T> get(): T = Bob[T::class]
inline fun <reified T> get(): T = ServiceLocator[T::class]
inline fun <reified T> inject(): Lazy<T> = lazy { Bob[T::class] }
inline fun <reified T> inject(): Lazy<T> = lazy { ServiceLocator[T::class] }

View file

@ -0,0 +1,16 @@
package com.pixelized.biblib.utils.mock
import com.pixelized.biblib.model.user.User
import java.util.*
class UserMock {
val user = User(
id = "",
created = Date(),
updated = Date(),
username = "R. Daneel Olivaw",
firstname = "R. Daneel",
lastname = "Olivaw",
amazonEmails = listOf("r.daneel.olivaw@biblib.com"),
)
}