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

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.model package com.pixelized.biblib.model.book
data class Author( data class Author(
val id: String, 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( data class Book(
val id: Int, val id: Int,

View file

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

View file

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

View file

@ -1,4 +1,4 @@
package com.pixelized.biblib.model package com.pixelized.biblib.model.book
data class Series( data class Series(
val id: String?, 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 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.query.AuthLoginQuery
import com.pixelized.biblib.network.data.response.AuthLoginResponse import com.pixelized.biblib.network.data.response.AuthLoginResponse
import com.pixelized.biblib.network.data.response.BookDetailResponse import com.pixelized.biblib.network.data.response.BookDetailResponse
import com.pixelized.biblib.network.data.response.BookListResponse import com.pixelized.biblib.network.data.response.BookListResponse
import com.pixelized.biblib.network.data.response.UserResponse
import retrofit2.http.* import retrofit2.http.*
interface IBibLibWebServiceAPI { interface IBibLibWebServiceAPI {
@ -13,8 +15,8 @@ interface IBibLibWebServiceAPI {
@GET("/authent/google-id-token") @GET("/authent/google-id-token")
suspend fun loginWithGoogle(@Query("id_token") token: String): AuthLoginResponse suspend fun loginWithGoogle(@Query("id_token") token: String): AuthLoginResponse
// @GET("/authent/user") @GET("/authent/user")
// suspend fun user(): UserResponse suspend fun user(): UserResponse
@GET("/api/book/new") @GET("/api/book/new")
suspend fun new(): BookListResponse suspend fun new(): BookListResponse
@ -25,9 +27,9 @@ interface IBibLibWebServiceAPI {
@GET("/api/book/{id}") @GET("/api/book/{id}")
suspend fun detail(@Path("id") bookId: Int): BookDetailResponse suspend fun detail(@Path("id") bookId: Int): BookDetailResponse
// @GET("/api/book/{id}/send/kindle") @GET("/api/book/{id}/send/kindle")
// suspend fun send(@Path("id") bookId: Int, @Query("mail") mail: String): LinkedTreeMap<String, Any> suspend fun send(@Path("id") bookId: Int, @Query("mail") mail: String): LinkedTreeMap<String, Any>
//
// @GET("/api/book/{id}/epub/url") // @GET("/api/book/{id}/epub/url")
// suspend fun epub(@Path("id") bookId: Int): TokenResponse // 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 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.BookDetailResponse
import com.pixelized.biblib.network.data.response.BookListResponse import com.pixelized.biblib.network.data.response.BookListResponse
import com.pixelized.biblib.utils.exception.MandatoryFieldMissingException import com.pixelized.biblib.utils.exception.MandatoryFieldMissingException
@ -9,7 +9,7 @@ import java.text.SimpleDateFormat
import java.util.* import java.util.*
class BookFactory { class BookFactory {
private val parser get() = SimpleDateFormat(FORMAT, Locale.getDefault()) private val parser get() = SimpleDateFormat(Factory.FORMAT, Locale.getDefault())
fun fromListResponseToBook( fun fromListResponseToBook(
response: BookListResponse.Book, response: BookListResponse.Book,
@ -160,8 +160,4 @@ class BookFactory {
isNew = isNew 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.crossref.BookGenreCrossRef
import com.pixelized.biblib.database.data.* import com.pixelized.biblib.database.data.*
import com.pixelized.biblib.database.relation.BookRelation 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 import com.pixelized.biblib.utils.injection.inject
class BookRepository : IBookRepository { class BookRepository : IBookRepository {

View file

@ -1,7 +1,7 @@
package com.pixelized.biblib.repository.book package com.pixelized.biblib.repository.book
import androidx.paging.DataSource import androidx.paging.DataSource
import com.pixelized.biblib.model.Book import com.pixelized.biblib.model.book.Book
interface IBookRepository { 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.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.Composable 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector 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 androidx.compose.ui.unit.dp
import com.pixelized.biblib.BuildConfig import com.pixelized.biblib.BuildConfig
import com.pixelized.biblib.R import com.pixelized.biblib.R
import com.pixelized.biblib.model.user.User
import com.pixelized.biblib.ui.theme.BibLibTheme import com.pixelized.biblib.ui.theme.BibLibTheme
import com.pixelized.biblib.ui.viewmodel.user.IUserViewModel
import java.util.* import java.util.*
@Composable @Composable
fun BibLibDrawer( fun BibLibDrawer(
userViewModel : IUserViewModel,
onNewClick: () -> Unit = {}, onNewClick: () -> Unit = {},
onBookClick: () -> Unit = {}, onBookClick: () -> Unit = {},
onSeriesClick: () -> Unit = {}, onSeriesClick: () -> Unit = {},
@ -45,13 +50,14 @@ fun BibLibDrawer(
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp) .padding(16.dp)
) { ) {
val user: State<User?> = userViewModel.user.observeAsState()
Text( Text(
style = typography.body1, style = typography.body1,
text = "R. Daneel Olivaw" text = user.value?.username ?: ""
) )
Text( Text(
style = typography.caption, 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 @Composable
fun BibLibDrawerLightPreview() { fun BibLibDrawerLightPreview() {
BibLibTheme { 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.BibLibTheme
import com.pixelized.biblib.ui.theme.Teal200 import com.pixelized.biblib.ui.theme.Teal200
import com.pixelized.biblib.utils.BitmapCache 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 import com.pixelized.biblib.utils.mock.BookThumbnailMock
private val THUMBNAIL_WIDTH: Dp = 60.dp private val THUMBNAIL_WIDTH: Dp = 60.dp
@ -157,7 +157,7 @@ private fun Placeholder(modifier: Modifier) = Surface(modifier = modifier, eleva
@Preview @Preview
@Composable @Composable
fun BookItemLightPreview() { fun BookItemLightPreview() {
Bob[BitmapCache::class] = BitmapCache(LocalContext.current) ServiceLocator[BitmapCache::class] = BitmapCache(LocalContext.current)
BibLibTheme { BibLibTheme {
val mock = BookThumbnailMock() val mock = BookThumbnailMock()
FilledBookItem(thumbnail = mock.bookThumbnail) FilledBookItem(thumbnail = mock.bookThumbnail)
@ -167,7 +167,7 @@ fun BookItemLightPreview() {
@Preview @Preview
@Composable @Composable
fun BookItemDarkPreview() { fun BookItemDarkPreview() {
Bob[BitmapCache::class] = BitmapCache(LocalContext.current) ServiceLocator[BitmapCache::class] = BitmapCache(LocalContext.current)
BibLibTheme(darkTheme = true) { BibLibTheme(darkTheme = true) {
val mock = BookThumbnailMock() val mock = BookThumbnailMock()
FilledBookItem(thumbnail = mock.bookThumbnail) FilledBookItem(thumbnail = mock.bookThumbnail)

View file

@ -1,7 +1,12 @@
package com.pixelized.biblib.ui.composable.pages 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.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll 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.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip 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.theme.Teal200
import com.pixelized.biblib.ui.viewmodel.book.BooksViewModel import com.pixelized.biblib.ui.viewmodel.book.BooksViewModel
import com.pixelized.biblib.ui.viewmodel.book.IBooksViewModel 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.BitmapCache
import com.pixelized.biblib.utils.injection.Bob import com.pixelized.biblib.utils.injection.ServiceLocator
import com.pixelized.biblib.utils.mock.BookMock import com.pixelized.biblib.utils.mock.BookMock
@Composable @Composable
fun DetailPage( fun DetailPage(
booksViewModel: IBooksViewModel = viewModel<BooksViewModel>(), booksViewModel: IBooksViewModel = viewModel<BooksViewModel>(),
userViewModel: IUserViewModel = viewModel<UserViewModel>(),
bookId: Int bookId: Int
) { ) {
Surface( Surface(
@ -51,15 +60,41 @@ fun DetailPage(
elevation = 4.dp elevation = 4.dp
) { ) {
val book by booksViewModel.getBookDetail(bookId).observeAsState() val book by booksViewModel.getBookDetail(bookId).observeAsState()
val user by userViewModel.user.observeAsState()
book?.let { 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 @Composable
fun DetailPage(book: BookUio) { @OptIn(ExperimentalAnimationApi::class)
fun DetailPage(
book: BookUio,
onEpubClick: () -> Unit = {},
onMobiClick: () -> Unit = {},
onSendClick: () -> Unit = {},
) {
val typography = MaterialTheme.typography val typography = MaterialTheme.typography
val pouet = remember { MutableTransitionState(false) }
Column( Column(
modifier = Modifier modifier = Modifier
@ -86,18 +121,21 @@ fun DetailPage(book: BookUio) {
Button( Button(
modifier = Modifier.padding(end = 4.dp), modifier = Modifier.padding(end = 4.dp),
imageVector = Icons.Default.Download, imageVector = Icons.Default.Download,
text = stringResource(id = R.string.action_epub) text = stringResource(id = R.string.action_epub),
) { } onClick = onEpubClick,
)
Button( Button(
modifier = Modifier.padding(horizontal = 4.dp), modifier = Modifier.padding(horizontal = 4.dp),
imageVector = Icons.Default.Download, imageVector = Icons.Default.Download,
text = stringResource(id = R.string.action_mobi), text = stringResource(id = R.string.action_mobi),
) { } onClick = onMobiClick,
)
Button( Button(
modifier = Modifier.padding(start = 4.dp), modifier = Modifier.padding(start = 4.dp),
imageVector = Icons.Default.Send, imageVector = Icons.Default.Send,
text = stringResource(id = R.string.action_send), text = stringResource(id = R.string.action_send),
) { } onClick = onSendClick,
)
} }
Text( Text(
modifier = Modifier modifier = Modifier
@ -141,6 +179,13 @@ fun DetailPage(book: BookUio) {
modifier = Modifier.padding(bottom = 16.dp) modifier = Modifier.padding(bottom = 16.dp)
) )
} }
AnimatedVisibility(
visibleState = pouet,
) {
SendMailBottomSheet()
}
} }
@Composable @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 @Preview
@Composable @Composable
fun DetailPageLightPreview() { fun DetailPageLightPreview() {
Bob[BitmapCache::class] = BitmapCache(LocalContext.current) ServiceLocator[BitmapCache::class] = BitmapCache(LocalContext.current)
BibLibTheme { BibLibTheme {
val mock = BookMock() val book = BookMock()
DetailPage(mock.book) DetailPage(book.book)
}
}
@Preview
@Composable
fun SendMailBottomSheetPreview() {
BibLibTheme {
SendMailBottomSheet()
} }
} }

View file

@ -1,10 +1,7 @@
package com.pixelized.biblib.ui.composable.screen package com.pixelized.biblib.ui.composable.screen
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.*
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween 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.Scaffold
import androidx.compose.material.rememberScaffoldState import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.* 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
import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel.Navigable.Page import com.pixelized.biblib.ui.viewmodel.navigation.INavigationViewModel.Navigable.Page
import com.pixelized.biblib.ui.viewmodel.navigation.NavigationViewModel 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 import kotlinx.coroutines.launch
@ -30,6 +29,7 @@ import kotlinx.coroutines.launch
fun HomeScreen( fun HomeScreen(
navigationViewModel: INavigationViewModel = viewModel<NavigationViewModel>(), navigationViewModel: INavigationViewModel = viewModel<NavigationViewModel>(),
booksViewModel: IBooksViewModel = viewModel<BooksViewModel>(), booksViewModel: IBooksViewModel = viewModel<BooksViewModel>(),
userViewModel: IUserViewModel = viewModel<UserViewModel>()
) { ) {
// navigation // navigation
val page by navigationViewModel.page.observeAsState() val page by navigationViewModel.page.observeAsState()
@ -59,6 +59,7 @@ fun HomeScreen(
}, },
drawerContent = { drawerContent = {
BibLibDrawer( BibLibDrawer(
userViewModel = userViewModel,
onNewClick = { onNewClick = {
coroutineScope.launch { scaffoldState.drawerState.close() } coroutineScope.launch { scaffoldState.drawerState.close() }
}, },
@ -85,20 +86,19 @@ fun HomeScreen(
AnimatedVisibility( AnimatedVisibility(
visible = page is Page.Detail, visible = page is Page.Detail,
initiallyVisible = false, enter = slideInHorizontally(
enter = slideInVertically(
animationSpec = tween(Animation.MEDIUM_DURATION), animationSpec = tween(Animation.MEDIUM_DURATION),
initialOffsetY = { height -> height }, initialOffsetX = { width -> width },
), ),
exit = slideOutVertically( exit = slideOutHorizontally(
animationSpec = tween(Animation.MEDIUM_DURATION), animationSpec = tween(Animation.MEDIUM_DURATION),
targetOffsetY = { height -> height }, targetOffsetX = { width -> width },
), ),
) { ) {
// Small trick to display the detail page during animation exit. // Small trick to display the detail page during animation exit.
var currentPage by remember { mutableStateOf<Page.Detail?>(null) } var currentPage by remember { mutableStateOf<Page.Detail?>(null) }
currentPage = page as? Page.Detail ?: currentPage 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 { BibLibTheme {
HomeScreen( HomeScreen(
INavigationViewModel.Mock(page = Page.Home.New), 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.content.Intent
import android.net.Uri import android.net.Uri
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* 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.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp 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 androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.biblib.R import com.pixelized.biblib.R
import com.pixelized.biblib.network.client.IBibLibClient.Companion.REGISTER_URL 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.ErrorCard
import com.pixelized.biblib.ui.composable.dialog.LoadingCard import com.pixelized.biblib.ui.composable.dialog.LoadingCard
import com.pixelized.biblib.ui.composable.dialog.SuccessCard import com.pixelized.biblib.ui.composable.dialog.SuccessCard
@ -110,85 +108,37 @@ private fun Dialogs(
val authState by authenticationViewModel.state.observeAsState() val authState by authenticationViewModel.state.observeAsState()
val bookState by booksViewModel.state.observeAsState() val bookState by booksViewModel.state.observeAsState()
val visible = authState !is IAuthenticationViewModel.State.Initial if (authState !is IAuthenticationViewModel.State.Initial) {
|| bookState !is IBooksViewModel.State.Initial val isInError = authState is IAuthenticationViewModel.State.Error
Dialog(
CrossFadeOverlay( onDismissRequest = { authenticationViewModel.clearState() },
modifier = Modifier.clickable { properties = DialogProperties(
if (visible) { dismissOnBackPress = isInError,
authenticationViewModel.clearState() dismissOnClickOutside = isInError
}
},
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( when {
message = stringResource(id = R.string.loading_authentication) bookState is IBooksViewModel.State.Error -> ErrorCard(
)
}
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), message = stringResource(id = R.string.error_book),
exception = (bookState as? IBooksViewModel.State.Error)?.exception exception = (bookState as? IBooksViewModel.State.Error)?.exception
) )
} bookState is IBooksViewModel.State.Loading -> LoadingCard(
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) message = stringResource(id = R.string.loading_book)
) )
} bookState is IBooksViewModel.State.Finished -> SuccessCard(
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) 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)
)
}
} }
} }
} }

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

View file

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

View file

@ -19,6 +19,8 @@ interface IBooksViewModel {
fun getBookDetail(id: Int): LiveData<BookUio> fun getBookDetail(id: Int): LiveData<BookUio>
fun send(id: Int, mail:String)
sealed class State { sealed class State {
object Initial : State() object Initial : State()
object Loading : State() object Loading : State()
@ -37,5 +39,6 @@ interface IBooksViewModel {
override fun updateBooks() = Unit override fun updateBooks() = Unit
override fun getBookDetail(id: Int) = MutableLiveData<BookUio>() 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 import kotlin.reflect.KClass
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
object Bob { object ServiceLocator {
private val components = hashMapOf<KClass<*>, Any>() private val components = hashMapOf<KClass<*>, Any>()
operator fun <I : Any, O : I> set(clazz: KClass<I>, component: O) { 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"),
)
}