From 6254ec4f028c2bfe9da90b78ea9b9ef0428f210f Mon Sep 17 00:00:00 2001 From: Thomas Andres Gomez Date: Sat, 27 Nov 2021 14:28:33 +0100 Subject: [PATCH] add user data fetch support. --- .../com/pixelized/biblib/BibLibApplication.kt | 23 ++-- .../biblib/model/{ => book}/Author.kt | 2 +- .../pixelized/biblib/model/{ => book}/Book.kt | 4 +- .../biblib/model/{ => book}/Genre.kt | 2 +- .../biblib/model/{ => book}/Language.kt | 2 +- .../biblib/model/{ => book}/Series.kt | 2 +- .../com/pixelized/biblib/model/user/User.kt | 13 ++ .../network/client/IBibLibWebServiceAPI.kt | 12 +- .../network/data/response/UserResponse.kt | 109 +++++++++++++++++ .../biblib/network/factory/BookFactory.kt | 8 +- .../biblib/network/factory/Factory.kt | 5 + .../biblib/network/factory/UserFactory.kt | 37 ++++++ .../biblib/repository/book/BookRepository.kt | 2 +- .../biblib/repository/book/IBookRepository.kt | 2 +- .../biblib/repository/user/IUserRepository.kt | 8 ++ .../biblib/repository/user/UserRepository.kt | 22 ++++ .../ui/composable/common/BibLibDrawer.kt | 14 ++- .../biblib/ui/composable/items/BookItem.kt | 6 +- .../biblib/ui/composable/pages/DetailPage.kt | 83 +++++++++++-- .../biblib/ui/composable/screen/HomeScreen.kt | 23 ++-- .../ui/composable/screen/LoginScreen.kt | 114 +++++------------- .../ui/composable/screen/SplashScreen.kt | 10 ++ .../ui/viewmodel/book/BooksViewModel.kt | 10 +- .../ui/viewmodel/book/IBooksViewModel.kt | 3 + .../ui/viewmodel/user/IUserViewModel.kt | 30 +++++ .../biblib/ui/viewmodel/user/UserViewModel.kt | 41 +++++++ .../injection/{Bob.kt => ServiceLocator.kt} | 6 +- .../pixelized/biblib/utils/mock/UserMock.kt | 16 +++ 28 files changed, 467 insertions(+), 142 deletions(-) rename app/src/main/java/com/pixelized/biblib/model/{ => book}/Author.kt (68%) rename app/src/main/java/com/pixelized/biblib/model/{ => book}/Book.kt (91%) rename app/src/main/java/com/pixelized/biblib/model/{ => book}/Genre.kt (60%) rename app/src/main/java/com/pixelized/biblib/model/{ => book}/Language.kt (80%) rename app/src/main/java/com/pixelized/biblib/model/{ => book}/Series.kt (72%) create mode 100644 app/src/main/java/com/pixelized/biblib/model/user/User.kt create mode 100644 app/src/main/java/com/pixelized/biblib/network/data/response/UserResponse.kt create mode 100644 app/src/main/java/com/pixelized/biblib/network/factory/Factory.kt create mode 100644 app/src/main/java/com/pixelized/biblib/network/factory/UserFactory.kt create mode 100644 app/src/main/java/com/pixelized/biblib/repository/user/IUserRepository.kt create mode 100644 app/src/main/java/com/pixelized/biblib/repository/user/UserRepository.kt create mode 100644 app/src/main/java/com/pixelized/biblib/ui/viewmodel/user/IUserViewModel.kt create mode 100644 app/src/main/java/com/pixelized/biblib/ui/viewmodel/user/UserViewModel.kt rename app/src/main/java/com/pixelized/biblib/utils/injection/{Bob.kt => ServiceLocator.kt} (74%) create mode 100644 app/src/main/java/com/pixelized/biblib/utils/mock/UserMock.kt diff --git a/app/src/main/java/com/pixelized/biblib/BibLibApplication.kt b/app/src/main/java/com/pixelized/biblib/BibLibApplication.kt index c02402f..3b34e94 100644 --- a/app/src/main/java/com/pixelized/biblib/BibLibApplication.kt +++ b/app/src/main/java/com/pixelized/biblib/BibLibApplication.kt @@ -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 { diff --git a/app/src/main/java/com/pixelized/biblib/model/Author.kt b/app/src/main/java/com/pixelized/biblib/model/book/Author.kt similarity index 68% rename from app/src/main/java/com/pixelized/biblib/model/Author.kt rename to app/src/main/java/com/pixelized/biblib/model/book/Author.kt index dbb0cb0..a13810d 100644 --- a/app/src/main/java/com/pixelized/biblib/model/Author.kt +++ b/app/src/main/java/com/pixelized/biblib/model/book/Author.kt @@ -1,4 +1,4 @@ -package com.pixelized.biblib.model +package com.pixelized.biblib.model.book data class Author( val id: String, diff --git a/app/src/main/java/com/pixelized/biblib/model/Book.kt b/app/src/main/java/com/pixelized/biblib/model/book/Book.kt similarity index 91% rename from app/src/main/java/com/pixelized/biblib/model/Book.kt rename to app/src/main/java/com/pixelized/biblib/model/book/Book.kt index 9495fe6..fbfe665 100644 --- a/app/src/main/java/com/pixelized/biblib/model/Book.kt +++ b/app/src/main/java/com/pixelized/biblib/model/book/Book.kt @@ -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, diff --git a/app/src/main/java/com/pixelized/biblib/model/Genre.kt b/app/src/main/java/com/pixelized/biblib/model/book/Genre.kt similarity index 60% rename from app/src/main/java/com/pixelized/biblib/model/Genre.kt rename to app/src/main/java/com/pixelized/biblib/model/book/Genre.kt index 723abd9..8b9ed55 100644 --- a/app/src/main/java/com/pixelized/biblib/model/Genre.kt +++ b/app/src/main/java/com/pixelized/biblib/model/book/Genre.kt @@ -1,4 +1,4 @@ -package com.pixelized.biblib.model +package com.pixelized.biblib.model.book data class Genre( val id: String, diff --git a/app/src/main/java/com/pixelized/biblib/model/Language.kt b/app/src/main/java/com/pixelized/biblib/model/book/Language.kt similarity index 80% rename from app/src/main/java/com/pixelized/biblib/model/Language.kt rename to app/src/main/java/com/pixelized/biblib/model/book/Language.kt index ceba158..ca61aa1 100644 --- a/app/src/main/java/com/pixelized/biblib/model/Language.kt +++ b/app/src/main/java/com/pixelized/biblib/model/book/Language.kt @@ -1,4 +1,4 @@ -package com.pixelized.biblib.model +package com.pixelized.biblib.model.book import java.util.* diff --git a/app/src/main/java/com/pixelized/biblib/model/Series.kt b/app/src/main/java/com/pixelized/biblib/model/book/Series.kt similarity index 72% rename from app/src/main/java/com/pixelized/biblib/model/Series.kt rename to app/src/main/java/com/pixelized/biblib/model/book/Series.kt index 9692b5d..38d442b 100644 --- a/app/src/main/java/com/pixelized/biblib/model/Series.kt +++ b/app/src/main/java/com/pixelized/biblib/model/book/Series.kt @@ -1,4 +1,4 @@ -package com.pixelized.biblib.model +package com.pixelized.biblib.model.book data class Series( val id: String?, diff --git a/app/src/main/java/com/pixelized/biblib/model/user/User.kt b/app/src/main/java/com/pixelized/biblib/model/user/User.kt new file mode 100644 index 0000000..bed18e3 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/model/user/User.kt @@ -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, +) \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/network/client/IBibLibWebServiceAPI.kt b/app/src/main/java/com/pixelized/biblib/network/client/IBibLibWebServiceAPI.kt index a752751..2c25e08 100644 --- a/app/src/main/java/com/pixelized/biblib/network/client/IBibLibWebServiceAPI.kt +++ b/app/src/main/java/com/pixelized/biblib/network/client/IBibLibWebServiceAPI.kt @@ -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 -// + @GET("/api/book/{id}/send/kindle") + suspend fun send(@Path("id") bookId: Int, @Query("mail") mail: String): LinkedTreeMap + // @GET("/api/book/{id}/epub/url") // suspend fun epub(@Path("id") bookId: Int): TokenResponse // diff --git a/app/src/main/java/com/pixelized/biblib/network/data/response/UserResponse.kt b/app/src/main/java/com/pixelized/biblib/network/data/response/UserResponse.kt new file mode 100644 index 0000000..a78c5e7 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/network/data/response/UserResponse.kt @@ -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? = 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? = null, + @SerializedName("ratings") + val ratings: List? = 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, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/network/factory/BookFactory.kt b/app/src/main/java/com/pixelized/biblib/network/factory/BookFactory.kt index 89b363b..498be90 100644 --- a/app/src/main/java/com/pixelized/biblib/network/factory/BookFactory.kt +++ b/app/src/main/java/com/pixelized/biblib/network/factory/BookFactory.kt @@ -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'" - } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/network/factory/Factory.kt b/app/src/main/java/com/pixelized/biblib/network/factory/Factory.kt new file mode 100644 index 0000000..469f33f --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/network/factory/Factory.kt @@ -0,0 +1,5 @@ +package com.pixelized.biblib.network.factory + +object Factory { + const val FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/network/factory/UserFactory.kt b/app/src/main/java/com/pixelized/biblib/network/factory/UserFactory.kt new file mode 100644 index 0000000..4d417f3 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/network/factory/UserFactory.kt @@ -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(), + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/repository/book/BookRepository.kt b/app/src/main/java/com/pixelized/biblib/repository/book/BookRepository.kt index 54935e8..f294a12 100644 --- a/app/src/main/java/com/pixelized/biblib/repository/book/BookRepository.kt +++ b/app/src/main/java/com/pixelized/biblib/repository/book/BookRepository.kt @@ -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 { diff --git a/app/src/main/java/com/pixelized/biblib/repository/book/IBookRepository.kt b/app/src/main/java/com/pixelized/biblib/repository/book/IBookRepository.kt index e1e0e62..34c056b 100644 --- a/app/src/main/java/com/pixelized/biblib/repository/book/IBookRepository.kt +++ b/app/src/main/java/com/pixelized/biblib/repository/book/IBookRepository.kt @@ -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 { diff --git a/app/src/main/java/com/pixelized/biblib/repository/user/IUserRepository.kt b/app/src/main/java/com/pixelized/biblib/repository/user/IUserRepository.kt new file mode 100644 index 0000000..1e266f5 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/repository/user/IUserRepository.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/repository/user/UserRepository.kt b/app/src/main/java/com/pixelized/biblib/repository/user/UserRepository.kt new file mode 100644 index 0000000..ee947f9 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/repository/user/UserRepository.kt @@ -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 { + return user?.amazonEmails ?: listOf() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/common/BibLibDrawer.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/common/BibLibDrawer.kt index 1dc5b97..714ce18 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/composable/common/BibLibDrawer.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/composable/common/BibLibDrawer.kt @@ -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 = 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() + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/items/BookItem.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/items/BookItem.kt index ba22021..275397d 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/composable/items/BookItem.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/composable/items/BookItem.kt @@ -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) diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/pages/DetailPage.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/pages/DetailPage.kt index dca3c27..3f2e836 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/composable/pages/DetailPage.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/composable/pages/DetailPage.kt @@ -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(), + userViewModel: IUserViewModel = viewModel(), 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() } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/screen/HomeScreen.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/screen/HomeScreen.kt index dcd7ae8..3e9cb8c 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/composable/screen/HomeScreen.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/composable/screen/HomeScreen.kt @@ -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(), booksViewModel: IBooksViewModel = viewModel(), + userViewModel: IUserViewModel = viewModel() ) { // 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(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() ) } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/screen/LoginScreen.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/screen/LoginScreen.kt index f10b4ad..391b3c7 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/composable/screen/LoginScreen.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/composable/screen/LoginScreen.kt @@ -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) - ) } } } diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/screen/SplashScreen.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/screen/SplashScreen.kt index 838cf48..6b7d054 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/composable/screen/SplashScreen.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/composable/screen/SplashScreen.kt @@ -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(), bookViewModel: IBooksViewModel = viewModel(), + userViewModel: IUserViewModel = viewModel(), navigationViewModel: INavigationViewModel = viewModel(), 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) } diff --git a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/book/BooksViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/book/BooksViewModel.kt index c7e69b5..533218e 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/book/BooksViewModel.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/book/BooksViewModel.kt @@ -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() diff --git a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/book/IBooksViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/book/IBooksViewModel.kt index 7fa5086..fe44b6d 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/book/IBooksViewModel.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/book/IBooksViewModel.kt @@ -19,6 +19,8 @@ interface IBooksViewModel { fun getBookDetail(id: Int): LiveData + 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() + override fun send(id: Int, mail: String) = Unit } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/user/IUserViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/user/IUserViewModel.kt new file mode 100644 index 0000000..a97e8b6 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/user/IUserViewModel.kt @@ -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 + val user: LiveData + + 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 = MutableLiveData() + override val user: LiveData = MutableLiveData(UserMock().user) + + override fun updateUser() = Unit + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/viewmodel/user/UserViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/user/UserViewModel.kt new file mode 100644 index 0000000..edd5ac1 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/viewmodel/user/UserViewModel.kt @@ -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() + override val state: LiveData get() = _state + + override val user: LiveData + get() = MutableLiveData().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)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/utils/injection/Bob.kt b/app/src/main/java/com/pixelized/biblib/utils/injection/ServiceLocator.kt similarity index 74% rename from app/src/main/java/com/pixelized/biblib/utils/injection/Bob.kt rename to app/src/main/java/com/pixelized/biblib/utils/injection/ServiceLocator.kt index 41ca89d..ac66488 100644 --- a/app/src/main/java/com/pixelized/biblib/utils/injection/Bob.kt +++ b/app/src/main/java/com/pixelized/biblib/utils/injection/ServiceLocator.kt @@ -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, Any>() operator fun set(clazz: KClass, component: O) { @@ -16,6 +16,6 @@ object Bob { } } -inline fun get(): T = Bob[T::class] +inline fun get(): T = ServiceLocator[T::class] -inline fun inject(): Lazy = lazy { Bob[T::class] } \ No newline at end of file +inline fun inject(): Lazy = lazy { ServiceLocator[T::class] } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/utils/mock/UserMock.kt b/app/src/main/java/com/pixelized/biblib/utils/mock/UserMock.kt new file mode 100644 index 0000000..ac681d6 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/utils/mock/UserMock.kt @@ -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"), + ) +} \ No newline at end of file