add user data fetch support.
This commit is contained in:
parent
9ce20ba374
commit
6254ec4f02
28 changed files with 467 additions and 142 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.biblib.model
|
||||
package com.pixelized.biblib.model.book
|
||||
|
||||
data class Author(
|
||||
val id: String,
|
||||
|
|
@ -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,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.biblib.model
|
||||
package com.pixelized.biblib.model.book
|
||||
|
||||
data class Genre(
|
||||
val id: String,
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.biblib.model
|
||||
package com.pixelized.biblib.model.book
|
||||
|
||||
import java.util.*
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.pixelized.biblib.model
|
||||
package com.pixelized.biblib.model.book
|
||||
|
||||
data class Series(
|
||||
val id: String?,
|
||||
13
app/src/main/java/com/pixelized/biblib/model/user/User.kt
Normal file
13
app/src/main/java/com/pixelized/biblib/model/user/User.kt
Normal 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>,
|
||||
)
|
||||
|
|
@ -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
|
||||
//
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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'"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.pixelized.biblib.network.factory
|
||||
|
||||
object Factory {
|
||||
const val FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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] }
|
||||
|
|
@ -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"),
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue