From 8be1ecc0a71fd65e3b33eb95880cde3fb414d693 Mon Sep 17 00:00:00 2001 From: Thomas Andres Gomez Date: Mon, 27 Mar 2023 13:09:20 +0200 Subject: [PATCH] Update plugin, & architecture clean up. --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 3 +- .../pixelized/biblib/module/FactoryModule.kt | 45 +++++ .../pixelized/biblib/module/NetworkModule.kt | 9 + .../biblib/module/RepositoryModule.kt | 24 ++- .../biblib/network/factory/BookFactory.kt | 7 +- .../biblib/network/factory/GenreFactory.kt | 2 +- .../biblib/network/factory/SeriesFactory.kt | 1 - .../biblib/network/factory/UserFactory.kt | 9 +- .../biblib/repository/book/BookRepository.kt | 131 ++++++++++-- .../biblib/repository/book/BookUtils.kt | 116 ----------- .../biblib/repository/book/IBookRepository.kt | 16 +- .../credential/CredentialRepository.kt | 3 +- .../repository/search/SearchRepository.kt | 5 +- .../biblib/repository/user/IUserRepository.kt | 4 +- .../biblib/repository/user/UserRepository.kt | 38 +++- .../biblib/ui/composable/StateUioHandler.kt | 84 -------- .../biblib/ui/composable/dialog/ErrorCard.kt | 77 ------- .../ui/composable/dialog/LoadingCard.kt | 72 ------- .../biblib/ui/composable/dialog/SuccesCard.kt | 68 ------- .../authentication/AuthenticationScreen.kt | 16 +- .../AuthenticationScreenContent.kt | 96 +++++---- .../viewModel/AuthenticationFormViewModel.kt | 32 ++- .../biblib/ui/screen/home/BooksViewModel.kt | 8 +- .../screen/home/detail/BookDetailViewModel.kt | 10 +- .../ui/screen/home/detail/DetailScreen.kt | 22 +- .../screen/home/page/profile/ProfilePage.kt | 189 ++++++++++-------- .../home/page/profile/ProfileViewModel.kt | 48 ++--- .../biblib/utils/extention/ModifierEx.kt | 25 ++- app/src/main/res/values-fr/strings.xml | 2 +- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 32 files changed, 486 insertions(+), 681 deletions(-) create mode 100644 app/src/main/java/com/pixelized/biblib/module/FactoryModule.kt delete mode 100644 app/src/main/java/com/pixelized/biblib/repository/book/BookUtils.kt delete mode 100644 app/src/main/java/com/pixelized/biblib/ui/composable/StateUioHandler.kt delete mode 100644 app/src/main/java/com/pixelized/biblib/ui/composable/dialog/ErrorCard.kt delete mode 100644 app/src/main/java/com/pixelized/biblib/ui/composable/dialog/LoadingCard.kt delete mode 100644 app/src/main/java/com/pixelized/biblib/ui/composable/dialog/SuccesCard.kt diff --git a/app/build.gradle b/app/build.gradle index 85fc741..214c0c0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -82,6 +82,7 @@ android { lint { disable 'MissingTranslation' } + namespace 'com.pixelized.biblib' } dependencies { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 273fa4b..d818dda 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + diff --git a/app/src/main/java/com/pixelized/biblib/module/FactoryModule.kt b/app/src/main/java/com/pixelized/biblib/module/FactoryModule.kt new file mode 100644 index 0000000..8eaf6eb --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/module/FactoryModule.kt @@ -0,0 +1,45 @@ +package com.pixelized.biblib.module + +import com.pixelized.biblib.network.factory.BookFactory +import com.pixelized.biblib.network.factory.GenreFactory +import com.pixelized.biblib.network.factory.SeriesFactory +import com.pixelized.biblib.network.factory.UserFactory +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import java.text.DateFormat +import java.util.* + +@Module +@InstallIn(SingletonComponent::class) +class FactoryModule { + + @Provides + fun provideBookFactory( + parser: DateFormat, + ): BookFactory { + return BookFactory( + parser = parser + ) + } + + @Provides + fun provideGenreFactory(): GenreFactory { + return GenreFactory() + } + + @Provides + fun provideSeriesFactory(): SeriesFactory { + return SeriesFactory() + } + + @Provides + fun provideUserFactory( + parser: DateFormat, + ): UserFactory { + return UserFactory( + parser = parser, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/module/NetworkModule.kt b/app/src/main/java/com/pixelized/biblib/module/NetworkModule.kt index 159925a..3f77102 100644 --- a/app/src/main/java/com/pixelized/biblib/module/NetworkModule.kt +++ b/app/src/main/java/com/pixelized/biblib/module/NetworkModule.kt @@ -5,6 +5,7 @@ import com.google.gson.Gson import com.google.gson.GsonBuilder import com.pixelized.biblib.network.client.BibLibClient import com.pixelized.biblib.network.client.IBibLibClient +import com.pixelized.biblib.network.factory.BIBLIB_API_DATE_FORMAT import com.pixelized.biblib.repository.connectivity.ConnectivityRepository import com.pixelized.biblib.repository.connectivity.IConnectivityRepository import dagger.Module @@ -12,6 +13,9 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* import javax.inject.Singleton @Module @@ -41,4 +45,9 @@ class NetworkModule { ): IConnectivityRepository { return ConnectivityRepository(context) } + + @Provides + fun provideBibLibDateFormatter(): DateFormat { + return SimpleDateFormat(BIBLIB_API_DATE_FORMAT, Locale.getDefault()) + } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/module/RepositoryModule.kt b/app/src/main/java/com/pixelized/biblib/module/RepositoryModule.kt index 17768b5..89a3a8d 100644 --- a/app/src/main/java/com/pixelized/biblib/module/RepositoryModule.kt +++ b/app/src/main/java/com/pixelized/biblib/module/RepositoryModule.kt @@ -2,10 +2,13 @@ package com.pixelized.biblib.module import android.app.Application import android.content.SharedPreferences -import androidx.room.PrimaryKey import com.google.gson.Gson import com.pixelized.biblib.database.BibLibDatabase import com.pixelized.biblib.network.client.IBibLibClient +import com.pixelized.biblib.network.factory.BookFactory +import com.pixelized.biblib.network.factory.GenreFactory +import com.pixelized.biblib.network.factory.SeriesFactory +import com.pixelized.biblib.network.factory.UserFactory import com.pixelized.biblib.repository.apiCache.APICacheRepository import com.pixelized.biblib.repository.apiCache.IAPICacheRepository import com.pixelized.biblib.repository.book.BookRepository @@ -21,7 +24,6 @@ import com.pixelized.biblib.repository.user.UserRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton @@ -64,10 +66,20 @@ class RepositoryModule { @Provides @Singleton fun provideBookRepository( - database: BibLibDatabase + client: IBibLibClient, + cache: IAPICacheRepository, + database: BibLibDatabase, + bookFactory: BookFactory, + seriesFactory: SeriesFactory, + genresFactory: GenreFactory, ): IBookRepository { return BookRepository( + client = client, + cache = cache, database = database, + bookFactory = bookFactory, + seriesFactory = seriesFactory, + genresFactory = genresFactory, ) } @@ -85,9 +97,13 @@ class RepositoryModule { @Singleton fun provideUserRepository( client: IBibLibClient, + factory: UserFactory, + preferences: SharedPreferences, ): IUserRepository { return UserRepository( - client = client + client = client, + factory = factory, + preference = preferences, ) } } \ 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 80856a5..0aec1b4 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 @@ -5,12 +5,13 @@ import com.pixelized.biblib.network.data.response.BookDetailResponse import com.pixelized.biblib.network.data.response.BookListResponse import com.pixelized.biblib.utils.exception.missingField import com.pixelized.biblib.utils.extention.toBoolean +import java.text.DateFormat import java.text.SimpleDateFormat import java.util.* -class BookFactory { - private val parser get() = SimpleDateFormat(BIBLIB_API_DATE_FORMAT, Locale.getDefault()) - +class BookFactory( + private val parser: DateFormat, +) { fun fromListResponseToBook( response: BookListResponse.Book, seriesHash: SeriesHash, diff --git a/app/src/main/java/com/pixelized/biblib/network/factory/GenreFactory.kt b/app/src/main/java/com/pixelized/biblib/network/factory/GenreFactory.kt index ae50e56..632ed40 100644 --- a/app/src/main/java/com/pixelized/biblib/network/factory/GenreFactory.kt +++ b/app/src/main/java/com/pixelized/biblib/network/factory/GenreFactory.kt @@ -3,9 +3,9 @@ package com.pixelized.biblib.network.factory import com.pixelized.biblib.model.book.Genre import com.pixelized.biblib.network.data.response.GenreListResponse import com.pixelized.biblib.utils.exception.missingField +import java.text.DateFormat class GenreFactory { - fun fromListResponseToGenreHash( response: GenreListResponse, ): Map> { diff --git a/app/src/main/java/com/pixelized/biblib/network/factory/SeriesFactory.kt b/app/src/main/java/com/pixelized/biblib/network/factory/SeriesFactory.kt index 0316204..40cc3bd 100644 --- a/app/src/main/java/com/pixelized/biblib/network/factory/SeriesFactory.kt +++ b/app/src/main/java/com/pixelized/biblib/network/factory/SeriesFactory.kt @@ -7,7 +7,6 @@ import com.pixelized.biblib.utils.exception.missingField typealias SeriesHash = Map> class SeriesFactory { - fun fromListResponseToSeriesHash( response: SeriesListResponse, ): SeriesHash { 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 index 082219e..637cbdc 100644 --- a/app/src/main/java/com/pixelized/biblib/network/factory/UserFactory.kt +++ b/app/src/main/java/com/pixelized/biblib/network/factory/UserFactory.kt @@ -4,12 +4,11 @@ import com.pixelized.biblib.model.user.DownloadedBooks import com.pixelized.biblib.model.user.User import com.pixelized.biblib.network.data.response.UserResponse import com.pixelized.biblib.utils.exception.missingField -import java.text.SimpleDateFormat -import java.util.* - -class UserFactory { - private val parser get() = SimpleDateFormat(BIBLIB_API_DATE_FORMAT, Locale.getDefault()) +import java.text.DateFormat +class UserFactory constructor( + private val parser: DateFormat, +) { fun fromUserResponseToUser( response: UserResponse ): User { 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 60fb717..6238068 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 @@ -1,6 +1,7 @@ package com.pixelized.biblib.repository.book +import android.util.Log import androidx.paging.DataSource import com.pixelized.biblib.database.BibLibDatabase import com.pixelized.biblib.database.crossref.BookAuthorCrossRef @@ -8,10 +9,25 @@ import com.pixelized.biblib.database.crossref.BookGenreCrossRef import com.pixelized.biblib.database.data.* import com.pixelized.biblib.database.factory.* import com.pixelized.biblib.model.book.* -import javax.inject.Inject +import com.pixelized.biblib.network.client.IBibLibClient +import com.pixelized.biblib.network.data.response.BookListResponse +import com.pixelized.biblib.network.data.response.GenreListResponse +import com.pixelized.biblib.network.data.response.SeriesListResponse +import com.pixelized.biblib.network.factory.BookFactory +import com.pixelized.biblib.network.factory.GenreFactory +import com.pixelized.biblib.network.factory.SeriesFactory +import com.pixelized.biblib.repository.apiCache.IAPICacheRepository +import com.pixelized.biblib.utils.exception.BookFetchException +import com.pixelized.biblib.utils.exception.NewBookFetchException +import kotlinx.coroutines.* -class BookRepository @Inject constructor( - private val database: BibLibDatabase +class BookRepository constructor( + private val client: IBibLibClient, + private val cache: IAPICacheRepository, + private val database: BibLibDatabase, + private val bookFactory: BookFactory, + private val seriesFactory: SeriesFactory, + private val genresFactory: GenreFactory, ) : IBookRepository { override fun getAll(): List = @@ -23,25 +39,110 @@ class BookRepository @Inject constructor( override fun getBookCount(): Int = database.bookDao().count() - override fun getNewsSource(): DataSource.Factory = - database.bookDao().getNews().map { it.toBook() } - override fun getBooksSource(): DataSource.Factory = database.bookDao().getBook().map { it.toBook() } - override fun getAuthorsSource(): DataSource.Factory = - database.authorDao().getAll().map { it.toAuthor() } + override fun getNewsSource(): DataSource.Factory = + database.bookDao().getNews().map { it.toBook() } - override fun getSeriesSource(): DataSource.Factory = - database.seriesDao().getAll().map { it.toSeries() } + override suspend fun getBookDetail(id: Int): Book { + val response = client.service.detail(id) + return bookFactory.fromDetailResponseToBook(response) + } - override fun getGenresSource(): DataSource.Factory = - database.genreDao().getAll().map { it.toGenre() } + override suspend fun update() = withContext(Dispatchers.IO) { + if (getBookCount() <= 0 || fetchNewBooks()) { + fetchAllBooks() + } + } - override fun getLanguagesSource(): DataSource.Factory = - database.languageDao().getAll().map { it.toLanguage() } + /** + * This method will fetch all new book from [client] and save them into the [cache] + * @param client the client used to fetch the data. + * @param cache the cache used to save the data. + * @return return true if the data have been loaded and book need to be updated. + */ + private suspend fun fetchNewBooks(): Boolean { + val cached = cache.new + val response: BookListResponse = client.service.new() - override suspend fun update(data: List) { + return when { + response.isError -> { + Log.e("loadNewBooks", response.message ?: "") + throw NewBookFetchException(response.message) + } + cached != response -> { + cache.new = response + true + } + else -> { + false + } + } + } + + + /** + * This method will fetch all book from [client] and save them into the [repository] + * @param client the client used to fetch the data. + * @param cache the cache used to read the new book data. + * @param repository the repository to save the book data. + * @return factory the factory use to convert dto to bo. + * @return this method will return true the books have been correctly fetch, otherwise false. + */ + private suspend fun fetchAllBooks(): Boolean = runBlocking { + val seriesAsync = async { client.service.series() } + val listAsync = async { client.service.list() } + val genreAsync = async { client.service.genre() } + val seriesResponse: SeriesListResponse = seriesAsync.await() + val bookResponse: BookListResponse = listAsync.await() + val genreResponse: GenreListResponse = genreAsync.await() + + when { + bookResponse.isError -> { + Log.e("loadAllBooks", bookResponse.message ?: "") + throw BookFetchException(bookResponse.message) + } + seriesResponse.isError -> { + Log.e("loadAllBooks", seriesResponse.message ?: "") + throw BookFetchException(seriesResponse.message) + } + genreResponse.isError -> { + Log.e("loadAllBooks", genreResponse.message ?: "") + throw BookFetchException(genreResponse.message) + } + else -> { + // prepare a list of new book ids. + val newIds = cache.new?.data?.map { it.book_id } ?: listOf() + // prepare a hash of series by books ids. + val series = seriesFactory.fromListResponseToSeriesHash(response = seriesResponse) + // prepare a hash of genre by books ids. + val genres = genresFactory.fromListResponseToGenreHash(response = genreResponse) + // parse books. + val books = bookResponse.data?.map { dto -> + val isNew = newIds.contains(dto.book_id) + val index = newIds.indexOf(dto.book_id) + return@map bookFactory.fromListResponseToBook( + response = dto, + seriesHash = series, + genreHash = genres, + isNew = isNew, + newOrder = index + ) + } + // check if data is valid + if (books.isNullOrEmpty()) { + Log.e("loadAllBooks", "Book list is empty") + throw BookFetchException("Book list is empty") + } + // update the database. + save(books) + true + } + } + } + + private fun save(data: List) { val authors = mutableSetOf() val genres = mutableSetOf() val series = mutableSetOf() diff --git a/app/src/main/java/com/pixelized/biblib/repository/book/BookUtils.kt b/app/src/main/java/com/pixelized/biblib/repository/book/BookUtils.kt deleted file mode 100644 index 949c7fb..0000000 --- a/app/src/main/java/com/pixelized/biblib/repository/book/BookUtils.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.pixelized.biblib.repository.book - -import android.util.Log -import com.pixelized.biblib.network.client.IBibLibClient -import com.pixelized.biblib.network.data.response.BookListResponse -import com.pixelized.biblib.network.data.response.GenreListResponse -import com.pixelized.biblib.network.data.response.SeriesListResponse -import com.pixelized.biblib.network.factory.BookFactory -import com.pixelized.biblib.network.factory.GenreFactory -import com.pixelized.biblib.network.factory.SeriesFactory -import com.pixelized.biblib.repository.apiCache.IAPICacheRepository -import com.pixelized.biblib.utils.exception.BookFetchException -import com.pixelized.biblib.utils.exception.NewBookFetchException - -suspend fun updateBooks( - client: IBibLibClient, - cache: IAPICacheRepository, - repository: IBookRepository, -) { - if (loadNewBooks(client, cache) || repository.getBookCount() <= 0) { - loadAllBooks(client, cache, repository) - } -} - -/** - * This method will fetch all new book from [client] and save them into the [cache] - * @param client the client used to fetch the data. - * @param cache the cache used to save the data. - * @return return true if the data have been loaded and book need to be updated. - */ -suspend fun loadNewBooks( - client: IBibLibClient, - cache: IAPICacheRepository, -): Boolean { - val cached = cache.new - val response: BookListResponse = client.service.new() - - return when { - response.isError -> { - Log.e("loadNewBooks", response.message ?: "") - throw NewBookFetchException(response.message) - } - cached != response -> { - cache.new = response - true - } - else -> { - false - } - } -} - - -/** - * This method will fetch all book from [client] and save them into the [repository] - * @param client the client used to fetch the data. - * @param cache the cache used to read the new book data. - * @param repository the repository to save the book data. - * @return factory the factory use to convert dto to bo. - * @return this method will return true the books have been correctly fetch, otherwise false. - */ -suspend fun loadAllBooks( - client: IBibLibClient, - cache: IAPICacheRepository, - repository: IBookRepository, - bookFactory: BookFactory = BookFactory(), - seriesFactory: SeriesFactory = SeriesFactory(), - genresFactory: GenreFactory = GenreFactory(), -): Boolean { - val seriesResponse: SeriesListResponse = client.service.series() - val bookResponse: BookListResponse = client.service.list() - val genreResponse: GenreListResponse = client.service.genre() - - return when { - bookResponse.isError -> { - Log.e("loadAllBooks", bookResponse.message ?: "") - throw BookFetchException(bookResponse.message) - } - seriesResponse.isError -> { - Log.e("loadAllBooks", seriesResponse.message ?: "") - throw BookFetchException(seriesResponse.message) - } - genreResponse.isError -> { - Log.e("loadAllBooks", genreResponse.message ?: "") - throw BookFetchException(genreResponse.message) - } - else -> { - // prepare a list of new book ids. - val newIds = cache.new?.data?.map { it.book_id } ?: listOf() - // prepare a hash of series by books ids. - val series = seriesFactory.fromListResponseToSeriesHash(response = seriesResponse) - // prepare a hash of genre by books ids. - val genres = genresFactory.fromListResponseToGenreHash(response = genreResponse) - // parse books. - val books = bookResponse.data?.map { dto -> - val isNew = newIds.contains(dto.book_id) - val index = newIds.indexOf(dto.book_id) - return@map bookFactory.fromListResponseToBook( - response = dto, - seriesHash = series, - genreHash = genres, - isNew = isNew, - newOrder = index - ) - } - // check if data is valid - if (books.isNullOrEmpty()) { - Log.e("loadAllBooks", "Book list is empty") - throw BookFetchException("Book list is empty") - } - // update the repository. - repository.update(books) - true - } - } -} \ No newline at end of file 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 6d5d857..ac4f668 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 @@ -5,23 +5,17 @@ import com.pixelized.biblib.model.book.* interface IBookRepository { + suspend fun update() + + suspend fun getBookDetail(id: Int): Book + fun getAll(): List fun getBook(id: Int): Book fun getBookCount(): Int - fun getNewsSource(): DataSource.Factory - fun getBooksSource(): DataSource.Factory - fun getAuthorsSource() : DataSource.Factory - - fun getSeriesSource(): DataSource.Factory - - fun getGenresSource(): DataSource.Factory - - fun getLanguagesSource(): DataSource.Factory - - suspend fun update(data: List) + fun getNewsSource(): DataSource.Factory } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/repository/credential/CredentialRepository.kt b/app/src/main/java/com/pixelized/biblib/repository/credential/CredentialRepository.kt index 276e041..79860e6 100644 --- a/app/src/main/java/com/pixelized/biblib/repository/credential/CredentialRepository.kt +++ b/app/src/main/java/com/pixelized/biblib/repository/credential/CredentialRepository.kt @@ -2,10 +2,9 @@ package com.pixelized.biblib.repository.credential import android.content.SharedPreferences import androidx.core.content.edit -import javax.inject.Inject -class CredentialRepository @Inject constructor( +class CredentialRepository constructor( private val preferences: SharedPreferences, ) : ICredentialRepository { diff --git a/app/src/main/java/com/pixelized/biblib/repository/search/SearchRepository.kt b/app/src/main/java/com/pixelized/biblib/repository/search/SearchRepository.kt index 943ac9a..341be7f 100644 --- a/app/src/main/java/com/pixelized/biblib/repository/search/SearchRepository.kt +++ b/app/src/main/java/com/pixelized/biblib/repository/search/SearchRepository.kt @@ -5,12 +5,11 @@ import com.pixelized.biblib.database.BibLibDatabase 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.factory.* import com.pixelized.biblib.model.book.* import com.pixelized.biblib.model.search.SortType -import com.pixelized.biblib.database.factory.* -import javax.inject.Inject -class SearchRepository @Inject constructor( +class SearchRepository constructor( private val database: BibLibDatabase ) : ISearchRepository { 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 index 1e266f5..5c88057 100644 --- a/app/src/main/java/com/pixelized/biblib/repository/user/IUserRepository.kt +++ b/app/src/main/java/com/pixelized/biblib/repository/user/IUserRepository.kt @@ -3,6 +3,6 @@ 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 + suspend fun user(forceUpdate: Boolean = false): User + suspend fun lastUpdateDuration(): Long } \ 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 index 27008c6..37e88f2 100644 --- a/app/src/main/java/com/pixelized/biblib/repository/user/UserRepository.kt +++ b/app/src/main/java/com/pixelized/biblib/repository/user/UserRepository.kt @@ -1,23 +1,43 @@ package com.pixelized.biblib.repository.user +import android.content.SharedPreferences +import androidx.core.content.edit import com.pixelized.biblib.model.user.User import com.pixelized.biblib.network.client.IBibLibClient import com.pixelized.biblib.network.factory.UserFactory -import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext -class UserRepository @Inject constructor( +class UserRepository( private val client: IBibLibClient, + private val factory: UserFactory, + private val preference: SharedPreferences, ) : IUserRepository { - private var user: User? = null + private var cache: User? = null - override suspend fun getUser(forceUpdate: Boolean): User { - return client.service.user().let { response -> - val factory = UserFactory() - factory.fromUserResponseToUser(response).also { user = it } + @Throws + override suspend fun user(forceUpdate: Boolean): User { + val local = cache + return when { + local == null || forceUpdate -> fetchUserAndCache() + else -> local } } - override suspend fun amazonEmails(): List { - return user?.amazonEmails ?: listOf() + override suspend fun lastUpdateDuration(): Long = withContext(Dispatchers.IO) { + System.currentTimeMillis() - preference.getLong(USER_LAST_UPDATE, 0L) + } + + @Throws + suspend fun fetchUserAndCache(): User = withContext(Dispatchers.IO) { + val response = client.service.user() + return@withContext factory.fromUserResponseToUser(response).also { + cache = it + preference.edit { putLong(USER_LAST_UPDATE, System.currentTimeMillis()) } + } + } + + companion object { + private const val USER_LAST_UPDATE = "USER_LAST_UPDATE" } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/StateUioHandler.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/StateUioHandler.kt deleted file mode 100644 index acb9fb6..0000000 --- a/app/src/main/java/com/pixelized/biblib/ui/composable/StateUioHandler.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.pixelized.biblib.ui.composable - -import androidx.compose.animation.* -import androidx.compose.animation.core.tween -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.window.Dialog -import com.pixelized.biblib.ui.composable.dialog.ErrorCard -import com.pixelized.biblib.ui.composable.dialog.LoadingCard -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.contract - -sealed class StateUio { - class Progress(val progress: Float? = null, val cache : T? = null) : StateUio() - class Failure(val exception: Exception, val cache: T? = null) : StateUio() - class Success(val value: T) : StateUio() -} - -@OptIn(ExperimentalContracts::class) -fun StateUio.isSuccessful(): Boolean { - contract { - returns(true) implies (this@isSuccessful is StateUio.Success) - } - return this is StateUio.Success -} - -@OptIn(ExperimentalAnimationApi::class) -@Composable -fun StateUioHandler( - state: StateUio?, - onDismissRequest: (StateUio) -> Unit = {}, - onSuccess: () -> Unit = { }, -) { - val currentOnDismissRequest by rememberUpdatedState(onDismissRequest) - val currentOnSuccess by rememberUpdatedState(onSuccess) - - when (state) { - is StateUio.Progress, - is StateUio.Failure -> { - Dialog(onDismissRequest = { currentOnDismissRequest(state) }) { - Box( - modifier = Modifier - .fillMaxSize() - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - onClick = { onDismissRequest(state) } - ), - contentAlignment = Alignment.Center - ) { - AnimatedContent( - targetState = state, - contentAlignment = Alignment.Center, - transitionSpec = { - fadeIn(tween(delayMillis = 150)) + scaleIn( - initialScale = 0.85f, - animationSpec = tween(delayMillis = 150) - ) with fadeOut(tween()) + scaleOut( - targetScale = 0.85f, - animationSpec = tween() - ) - }, - ) { - when (it) { - is StateUio.Progress -> LoadingCard() - is StateUio.Failure -> ErrorCard(exception = it.exception) - else -> Unit // nothing to do. - } - } - } - } - } - is StateUio.Success -> currentOnSuccess() - null -> Unit // nothing to do. - } -} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/dialog/ErrorCard.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/dialog/ErrorCard.kt deleted file mode 100644 index f008a19..0000000 --- a/app/src/main/java/com/pixelized/biblib/ui/composable/dialog/ErrorCard.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.pixelized.biblib.ui.composable.dialog - -import android.content.res.Configuration.UI_MODE_NIGHT_NO -import android.content.res.Configuration.UI_MODE_NIGHT_YES -import androidx.compose.foundation.layout.* -import androidx.compose.material.Card -import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.sharp.ErrorOutline -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import com.pixelized.biblib.R -import com.pixelized.biblib.ui.theme.BibLibTheme -import com.pixelized.biblib.utils.exception.NoBearerException -import com.pixelized.biblib.utils.extention.bibLib - - -@Composable -fun ErrorCard( - modifier: Modifier = Modifier, - message: String = stringResource(id = R.string.error_generic), - exception: Exception? = null, -) { - Card( - modifier = modifier, - elevation = MaterialTheme.bibLib.dimen.dialog.elevation, - ) { - Column( - modifier = Modifier - .padding(MaterialTheme.bibLib.dimen.dp16) - .sizeIn( - minWidth = MaterialTheme.bibLib.dimen.dialog.minimum.width, - minHeight = MaterialTheme.bibLib.dimen.dialog.minimum.height, - ), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon( - modifier = Modifier.size(MaterialTheme.bibLib.dimen.dialog.iconSize), - tint = MaterialTheme.colors.error, - imageVector = Icons.Sharp.ErrorOutline, - contentDescription = null - ) - if (message.isNotEmpty()) { - Text( - modifier = Modifier.padding(top = MaterialTheme.bibLib.dimen.dp16), - style = MaterialTheme.typography.body1, - textAlign = TextAlign.Center, - text = message - ) - } - if (exception != null) { - Text( - modifier = Modifier.padding(top = MaterialTheme.bibLib.dimen.dp8), - style = MaterialTheme.typography.caption, - textAlign = TextAlign.Center, - text = exception.message ?: exception::class.java.simpleName - ) - } - } - } -} - -@Composable -@Preview(uiMode = UI_MODE_NIGHT_NO) -@Preview(uiMode = UI_MODE_NIGHT_YES) -private fun ErrorCardLightPreview() { - BibLibTheme { - ErrorCard(exception = NoBearerException()) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/dialog/LoadingCard.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/dialog/LoadingCard.kt deleted file mode 100644 index cdb5df6..0000000 --- a/app/src/main/java/com/pixelized/biblib/ui/composable/dialog/LoadingCard.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.pixelized.biblib.ui.composable.dialog - -import android.content.res.Configuration -import androidx.compose.foundation.layout.* -import androidx.compose.material.Card -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import com.pixelized.biblib.R -import com.pixelized.biblib.ui.theme.BibLibTheme -import com.pixelized.biblib.utils.extention.bibLib - - -@Composable -fun LoadingCard( - modifier: Modifier = Modifier, - progress: Float? = null, - message: String? = null, -) { - Card( - modifier = modifier, - elevation = MaterialTheme.bibLib.dimen.dialog.elevation, - ) { - Column( - modifier = Modifier - .padding(MaterialTheme.bibLib.dimen.dp16) - .sizeIn( - minWidth = MaterialTheme.bibLib.dimen.dialog.minimum.width, - minHeight = MaterialTheme.bibLib.dimen.dialog.minimum.height, - ), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - if (progress == null) { - CircularProgressIndicator( - modifier = Modifier.size(MaterialTheme.bibLib.dimen.dialog.iconSize) - ) - } else { - CircularProgressIndicator( - modifier = Modifier.size(MaterialTheme.bibLib.dimen.dialog.iconSize), - progress = progress, - ) - } - if (message?.isNotEmpty() == true) { - val typography = MaterialTheme.typography - Text( - modifier = Modifier.padding(top = MaterialTheme.bibLib.dimen.dp16), - style = typography.body1, - textAlign = TextAlign.Center, - text = message - ) - } - } - } -} - -@Composable -@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun LoadingCardLightPreview() { - BibLibTheme { - LoadingCard( - message = stringResource(id = R.string.loading_authentication) - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/composable/dialog/SuccesCard.kt b/app/src/main/java/com/pixelized/biblib/ui/composable/dialog/SuccesCard.kt deleted file mode 100644 index 510213f..0000000 --- a/app/src/main/java/com/pixelized/biblib/ui/composable/dialog/SuccesCard.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.pixelized.biblib.ui.composable.dialog - -import android.content.res.Configuration -import androidx.compose.foundation.layout.* -import androidx.compose.material.Card -import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.sharp.Done -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import com.pixelized.biblib.R -import com.pixelized.biblib.ui.theme.BibLibTheme -import com.pixelized.biblib.utils.extention.bibLib - - -@Composable -fun SuccessCard( - modifier: Modifier = Modifier, - message: String? = null, -) { - Card( - modifier = modifier, - elevation = MaterialTheme.bibLib.dimen.dialog.elevation, - ) { - Column( - modifier = Modifier - .padding(MaterialTheme.bibLib.dimen.dp16) - .sizeIn( - minWidth = MaterialTheme.bibLib.dimen.dialog.minimum.width, - minHeight = MaterialTheme.bibLib.dimen.dialog.minimum.height, - ), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon( - modifier = Modifier.size(MaterialTheme.bibLib.dimen.dialog.iconSize), - tint = MaterialTheme.colors.primary, - imageVector = Icons.Sharp.Done, - contentDescription = null, - ) - if (message != null) { - Text( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(top = MaterialTheme.bibLib.dimen.dp16), - style = MaterialTheme.typography.body1, - textAlign = TextAlign.Center, - text = message - ) - } - } - } -} - -@Composable -@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun SuccessLightPreview() { - BibLibTheme { - SuccessCard(message = stringResource(id = R.string.success_authentication)) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/authentication/AuthenticationScreen.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/authentication/AuthenticationScreen.kt index bdb237a..c2f3035 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/screen/authentication/AuthenticationScreen.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/authentication/AuthenticationScreen.kt @@ -72,18 +72,10 @@ fun AuthenticationScreen( login = formViewModel.form.login, password = formViewModel.form.password, rememberPassword = formViewModel.form.remember, - onLoginChange = { - formViewModel.onLoginChange(it) - }, - onPasswordChange = { - formViewModel.onPasswordChange(it) - }, - onRememberPasswordChange = { - formViewModel.onRememberChange(it) - }, - onGoogleSignIn = { - authenticationViewModel.loginWithGoogle() - }, + onLoginChange = formViewModel::onLoginChange, + onPasswordChange = formViewModel::onPasswordChange, + onRememberPasswordChange = formViewModel::onRememberChange, + onGoogleSignIn = authenticationViewModel::loginWithGoogle, onSignIn = { authenticationViewModel.login( login = formViewModel.form.login, diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/authentication/AuthenticationScreenContent.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/authentication/AuthenticationScreenContent.kt index fc8de20..79cd73d 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/screen/authentication/AuthenticationScreenContent.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/authentication/AuthenticationScreenContent.kt @@ -247,44 +247,66 @@ private fun CredentialRemember( } @Composable -@ReadOnlyComposable -private fun googleStringResource(): AnnotatedString = buildAnnotatedString { +private fun googleStringResource(): AnnotatedString { val default = LocalTextStyle.current.toSpanStyle() - withStyle( - style = default - ) { - append(stringResource(id = R.string.action_google_sign_in)) - append(" ") - } - withStyle( - style = default.copy(color = GoogleColorPalette.blue, fontWeight = FontWeight.ExtraBold), - ) { - append("G") - } - withStyle( - style = default.copy(color = GoogleColorPalette.red, fontWeight = FontWeight.ExtraBold), - ) { - append("o") - } - withStyle( - style = default.copy(color = GoogleColorPalette.yellow, fontWeight = FontWeight.ExtraBold), - ) { - append("o") - } - withStyle( - style = default.copy(color = GoogleColorPalette.blue, fontWeight = FontWeight.ExtraBold), - ) { - append("g") - } - withStyle( - style = default.copy(color = GoogleColorPalette.green, fontWeight = FontWeight.ExtraBold), - ) { - append("l") - } - withStyle( - style = default.copy(color = GoogleColorPalette.red, fontWeight = FontWeight.ExtraBold), - ) { - append("e") + val google = stringResource(id = R.string.action_google_sign_in) + return remember { + buildAnnotatedString { + withStyle( + style = default + ) { + append(google) + append(" ") + } + withStyle( + style = default.copy( + color = GoogleColorPalette.blue, + fontWeight = FontWeight.ExtraBold + ), + ) { + append("G") + } + withStyle( + style = default.copy( + color = GoogleColorPalette.red, + fontWeight = FontWeight.ExtraBold + ), + ) { + append("o") + } + withStyle( + style = default.copy( + color = GoogleColorPalette.yellow, + fontWeight = FontWeight.ExtraBold + ), + ) { + append("o") + } + withStyle( + style = default.copy( + color = GoogleColorPalette.blue, + fontWeight = FontWeight.ExtraBold + ), + ) { + append("g") + } + withStyle( + style = default.copy( + color = GoogleColorPalette.green, + fontWeight = FontWeight.ExtraBold + ), + ) { + append("l") + } + withStyle( + style = default.copy( + color = GoogleColorPalette.red, + fontWeight = FontWeight.ExtraBold + ), + ) { + append("e") + } + } } } diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/authentication/viewModel/AuthenticationFormViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/authentication/viewModel/AuthenticationFormViewModel.kt index 3cd377d..75c6db6 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/screen/authentication/viewModel/AuthenticationFormViewModel.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/authentication/viewModel/AuthenticationFormViewModel.kt @@ -1,6 +1,5 @@ package com.pixelized.biblib.ui.screen.authentication.viewModel -import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.pixelized.biblib.repository.credential.ICredentialRepository @@ -13,26 +12,15 @@ class AuthenticationFormViewModel @Inject constructor( private val credentialRepository: ICredentialRepository, ) : ViewModel() { - private val _login: MutableState - private val _password: MutableState + private val _login = mutableStateOf(credentialRepository.rememberedLogin) + private val _password = mutableStateOf(credentialRepository.rememberedPassword) private val _remember = mutableStateOf(credentialRepository.rememberCredential) - val form: AuthenticationFormUIO - get() = AuthenticationFormUIO( - login = _login, - password = _password, - remember = _remember, - ) - - init { - if (credentialRepository.rememberCredential) { - _login = mutableStateOf(credentialRepository.login ?: "") - _password = mutableStateOf(credentialRepository.password ?: "") - } else { - _login = mutableStateOf("") - _password = mutableStateOf("") - } - } + val form: AuthenticationFormUIO = AuthenticationFormUIO( + login = _login, + password = _password, + remember = _remember, + ) fun onLoginChange(login: String) { // update login in the repository @@ -66,4 +54,10 @@ class AuthenticationFormViewModel @Inject constructor( // update the UI State _remember.value = remember } + + private val ICredentialRepository.rememberedLogin: String + get() = if (rememberCredential) login ?: "" else "" + + private val ICredentialRepository.rememberedPassword: String + get() = if (rememberCredential) password ?: "" else "" } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/BooksViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/BooksViewModel.kt index 670df14..32d245d 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/screen/home/BooksViewModel.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/BooksViewModel.kt @@ -38,13 +38,7 @@ class BooksViewModel @Inject constructor( viewModelScope.launch { isLoading = true try { - withContext(Dispatchers.IO) { - com.pixelized.biblib.repository.book.updateBooks( - client = client, - cache = cache, - repository = repository, - ) - } + repository.update() } catch (exception: Exception) { Log.e("BooksViewModel", exception.message, exception) _updateError.emit(BookUpdateErrorUio()) diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/BookDetailViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/BookDetailViewModel.kt index 920d6d8..9ae6d4b 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/BookDetailViewModel.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/BookDetailViewModel.kt @@ -6,8 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.pixelized.biblib.R import com.pixelized.biblib.network.client.IBibLibClient -import com.pixelized.biblib.network.factory.BookFactory -import com.pixelized.biblib.repository.book.BookRepository +import com.pixelized.biblib.repository.book.IBookRepository import com.pixelized.biblib.utils.extention.toDetailUio import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers @@ -20,7 +19,7 @@ import javax.inject.Inject @HiltViewModel class BookDetailViewModel @Inject constructor( - private val bookRepository: BookRepository, + private val bookRepository: IBookRepository, private val client: IBibLibClient, ) : ViewModel() { @@ -78,10 +77,7 @@ class BookDetailViewModel @Inject constructor( } private suspend fun getBookDetail(id: Int): BookDetailUio { - val factory = BookFactory() - val response = client.service.detail(id) - val book = factory.fromDetailResponseToBook(response) - return book.toDetailUio(placeHolder = false) + return bookRepository.getBookDetail(id = id).toDetailUio(placeHolder = false) } private fun toDetailErrorUio(bookId: Int) = BookDetailUioErrorUio.GetDetailInput( diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/DetailScreen.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/DetailScreen.kt index a37e253..cad4f81 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/DetailScreen.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/DetailScreen.kt @@ -14,13 +14,11 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.hilt.navigation.compose.hiltViewModel import com.pixelized.biblib.R import com.pixelized.biblib.ui.LocalSnackHostState -import com.pixelized.biblib.ui.composable.StateUio import com.pixelized.biblib.ui.scaffold.LocalDetailBottomSheetState import com.pixelized.biblib.ui.screen.home.page.profile.ProfileViewModel import com.pixelized.biblib.ui.screen.home.page.profile.UserUio import com.pixelized.biblib.ui.theme.color.ShadowPalette import com.pixelized.biblib.utils.extention.bibLib -import kotlinx.coroutines.cancel import kotlinx.coroutines.launch @Stable @@ -98,12 +96,12 @@ fun DetailScreen( .systemBarsPadding(), book = detail, onSend = { - onSend( + send( context = context, profileViewModel = profileViewModel, snackBarHost = snackBarHost, emailSheetState = emailSheetState, - userState = profileViewModel.user + user = profileViewModel.user ) } ) @@ -158,31 +156,31 @@ fun DetailScreen( } @OptIn(ExperimentalMaterialApi::class) -private suspend fun onSend( +private suspend fun send( context: Context, profileViewModel: ProfileViewModel, snackBarHost: SnackbarHostState, emailSheetState: ModalBottomSheetState, - userState: StateUio, + user: State, ) { - val user = userState as? StateUio.Success - if (user?.value?.amazonEmails?.isEmpty() == true) { + if (user.value?.amazonEmails?.isNotEmpty() == true) { + emailSheetState.show() + } else { val result = snackBarHost.showSnackbar( message = context.getString(R.string.error_send_no_amazon_email_message), actionLabel = context.getString(R.string.error_send_no_amazon_email_action), duration = SnackbarDuration.Indefinite, ) if (result == SnackbarResult.ActionPerformed) { - onSend( + profileViewModel.updateUser() + send( context = context, profileViewModel = profileViewModel, snackBarHost = snackBarHost, emailSheetState = emailSheetState, - userState = profileViewModel.updateUser() + user = user ) } - } else { - emailSheetState.show() } } diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/profile/ProfilePage.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/profile/ProfilePage.kt index 2e1c0a8..a43ad8b 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/profile/ProfilePage.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/profile/ProfilePage.kt @@ -8,23 +8,24 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.* -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Edit -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.pixelized.biblib.R import com.pixelized.biblib.network.client.IBibLibClient -import com.pixelized.biblib.ui.composable.StateUio import com.pixelized.biblib.ui.navigation.LocalScreenNavHostController import com.pixelized.biblib.ui.navigation.navigateToAuthentication import com.pixelized.biblib.ui.theme.BibLibTheme import com.pixelized.biblib.utils.extention.bibLib import com.pixelized.biblib.utils.extention.default +import com.pixelized.biblib.utils.extention.placeholder @Composable fun ProfilePage( @@ -34,35 +35,38 @@ fun ProfilePage( val navigation = LocalScreenNavHostController.current Card { - when (val user = viewModel.user) { - is StateUio.Progress -> Unit - is StateUio.Success -> ProfileScreenContent( - modifier = Modifier - .fillMaxWidth() - .padding(MaterialTheme.bibLib.dimen.dp16), - user = user.value, - onEditClick = { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(IBibLibClient.EDIT_PROFILE)) - context.startActivity(intent) - }, - onLogoutClick = { - viewModel.logout() - navigation.navigateToAuthentication() - } - ) - is StateUio.Failure -> Unit - } + ProfileScreenContent( + modifier = Modifier + .fillMaxWidth() + .padding(MaterialTheme.bibLib.dimen.dp16), + user = viewModel.user, + onEditClick = { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(IBibLibClient.EDIT_PROFILE)) + context.startActivity(intent) + }, + onLogoutClick = { + viewModel.logout() + navigation.navigateToAuthentication() + } + ) } } @Composable private fun ProfileScreenContent( modifier: Modifier = Modifier, - user: UserUio, + user: State, onEditClick: () -> Unit = default(), onLogoutClick: () -> Unit = default(), ) { - Column(modifier = modifier) { + val placeholder: Boolean by remember(user) { + derivedStateOf { user.value == null } + } + + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { Text( style = MaterialTheme.typography.body1, color = MaterialTheme.colors.onSurface, @@ -70,29 +74,28 @@ private fun ProfileScreenContent( ) Text( + modifier = Modifier.placeholder { placeholder }, style = MaterialTheme.typography.h6, color = MaterialTheme.colors.primary, - text = user.username, + text = user.value?.username ?: "DefinitelyNotARobot", ) - if (user.firstname?.isNotEmpty() == true || user.lastname?.isNotEmpty() == true) { - Row { - user.firstname?.let { - Text( - modifier = Modifier.padding(end = MaterialTheme.bibLib.dimen.dp4), - style = MaterialTheme.typography.body1, - color = MaterialTheme.colors.onSurface, - text = it, - ) - } - user.lastname?.let { - Text( - style = MaterialTheme.typography.body1, - color = MaterialTheme.colors.onSurface, - text = it, - ) - } - } + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + modifier = Modifier.placeholder { placeholder }, + style = MaterialTheme.typography.body1, + color = MaterialTheme.colors.onSurface, + text = user.value?.firstname ?: if (placeholder) "R. Daneel" else "", + ) + + Text( + modifier = Modifier.placeholder { placeholder }, + style = MaterialTheme.typography.body1, + color = MaterialTheme.colors.onSurface, + text = user.value?.lastname ?: if (placeholder) "Olivaw" else "", + ) } Text( @@ -101,18 +104,11 @@ private fun ProfileScreenContent( color = MaterialTheme.colors.onSurface, text = stringResource(id = R.string.profile_emails) ) + LazyColumn { - if (user.amazonEmails.isEmpty()) { - item { - Text( - style = MaterialTheme.typography.caption, - color = MaterialTheme.colors.error, - text = stringResource(id = R.string.profile_emails_empty), - ) - } - } else { + if (user.value?.amazonEmails?.isNotEmpty() == true) { items( - items = user.amazonEmails + items = user.value?.amazonEmails ?: emptyList() ) { Text( style = MaterialTheme.typography.caption, @@ -120,35 +116,41 @@ private fun ProfileScreenContent( text = it, ) } + } else { + item { + Text( + modifier = Modifier.placeholder { placeholder }, + style = MaterialTheme.typography.caption, + color = MaterialTheme.colors.error, + text = stringResource(id = R.string.profile_emails_empty), + ) + } } } - Button( + Row( modifier = Modifier .padding(top = MaterialTheme.bibLib.dimen.dp16) .align(Alignment.End), - colors = ButtonDefaults.outlinedButtonColors(), - onClick = onEditClick, + horizontalArrangement = Arrangement.spacedBy(4.dp), ) { - Icon( - imageVector = Icons.Default.Edit, - contentDescription = null - ) - Text( - modifier = Modifier.padding(start = MaterialTheme.bibLib.dimen.dp8), - text = stringResource(id = R.string.profile_edit_action) - ) - } + Button( + colors = ButtonDefaults.textButtonColors(), + onClick = onEditClick, + ) { + Text( + text = stringResource(id = R.string.profile_edit_action) + ) + } - Button( - modifier = Modifier - .align(Alignment.End), - colors = ButtonDefaults.outlinedButtonColors(), - onClick = onLogoutClick, - ) { - Text( - text = stringResource(id = R.string.profile_logout_action) - ) + Button( + colors = ButtonDefaults.textButtonColors(), + onClick = onLogoutClick, + ) { + Text( + text = stringResource(id = R.string.profile_logout_action) + ) + } } } } @@ -156,23 +158,42 @@ private fun ProfileScreenContent( @Composable @Preview(showBackground = true, uiMode = UI_MODE_NIGHT_NO) @Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES) -private fun ProfileScreenContentPreview() { +private fun ProfileScreenContentPreview( + @PreviewParameter(ProfilePreviewProvider::class) preview: State +) { BibLibTheme { Box { ProfileScreenContent( modifier = Modifier .fillMaxWidth() .padding(all = MaterialTheme.bibLib.dimen.dp16), - user = UserUio( - username = "DefinitelyNotARobot", - firstname = "R. Daneel", - lastname = "Olivaw", - amazonEmails = listOf( - "r.daneel.olivaw@robot.com", - "r.daneel.olivaw@biblib.com", - ), - ), + user = preview, ) } } +} + +class ProfilePreviewProvider : PreviewParameterProvider> { + override val values: Sequence> = sequenceOf( + mutableStateOf(null), + mutableStateOf( + UserUio( + username = "DefinitelyNotARobot", + firstname = null, + lastname = null, + amazonEmails = emptyList(), + ) + ), + mutableStateOf( + UserUio( + username = "DefinitelyNotARobot", + firstname = "R. Daneel", + lastname = "Olivaw", + amazonEmails = listOf( + "r.daneel.olivaw@robot.com", + "r.daneel.olivaw@biblib.com", + ), + ) + ), + ) } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/profile/ProfileViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/profile/ProfileViewModel.kt index f81fa9a..6c7009e 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/profile/ProfileViewModel.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/page/profile/ProfileViewModel.kt @@ -1,37 +1,37 @@ package com.pixelized.biblib.ui.screen.home.page.profile import android.util.Log +import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.pixelized.biblib.model.user.User -import com.pixelized.biblib.network.client.IBibLibClient -import com.pixelized.biblib.network.factory.UserFactory import com.pixelized.biblib.repository.credential.ICredentialRepository import com.pixelized.biblib.repository.googleSignIn.IGoogleSingInRepository -import com.pixelized.biblib.ui.composable.StateUio +import com.pixelized.biblib.repository.user.IUserRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import javax.inject.Inject @HiltViewModel class ProfileViewModel @Inject constructor( private val credentialRepository: ICredentialRepository, + private val userRepository: IUserRepository, private val googleSignIn: IGoogleSingInRepository, - private val client: IBibLibClient, ) : ViewModel() { - var user by mutableStateOf>(StateUio.Progress()) - private set + private val _user = mutableStateOf(null) + val user: State = _user - val mails: List by derivedStateOf { - (user as? StateUio.Success)?.value?.amazonEmails ?: emptyList() - } + private val _error = MutableSharedFlow() + val error: Flow = _error + + val mails: List by derivedStateOf { user.value?.amazonEmails ?: emptyList() } init { viewModelScope.launch(Dispatchers.IO) { @@ -39,19 +39,15 @@ class ProfileViewModel @Inject constructor( } } - suspend fun updateUser(): StateUio { - return withContext(Dispatchers.IO) { - try { - val factory = UserFactory() - val response = client.service.user() - val data = factory.fromUserResponseToUser(response) - StateUio.Success(data.toUio()) - } catch (exception: Exception) { - Log.e("AccountViewModel", exception.message, exception) - StateUio.Failure(exception) - }.also { - user = it - } + suspend fun updateUser() { + try { + val data = userRepository.user( + forceUpdate = userRepository.lastUpdateDuration() > DAY_IN_MILLIS + ) + _user.value = data.toUio() + } catch (exception: Exception) { + Log.e("AccountViewModel", exception.message, exception) + _error.emit(exception) } } @@ -66,4 +62,8 @@ class ProfileViewModel @Inject constructor( lastname = lastname, amazonEmails = amazonEmails.toList(), ) + + companion object { + private const val DAY_IN_MILLIS = 86400000 + } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/utils/extention/ModifierEx.kt b/app/src/main/java/com/pixelized/biblib/utils/extention/ModifierEx.kt index 17f11dc..215a04e 100644 --- a/app/src/main/java/com/pixelized/biblib/utils/extention/ModifierEx.kt +++ b/app/src/main/java/com/pixelized/biblib/utils/extention/ModifierEx.kt @@ -1,15 +1,24 @@ package com.pixelized.biblib.utils.extention +import android.annotation.SuppressLint +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.autofill.AutofillNode import androidx.compose.ui.autofill.AutofillType import androidx.compose.ui.composed import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalAutofill import androidx.compose.ui.platform.LocalAutofillTree +import com.google.accompanist.placeholder.PlaceholderHighlight +import com.google.accompanist.placeholder.placeholder +import com.google.accompanist.placeholder.shimmer @OptIn(ExperimentalComposeUiApi::class) fun Modifier.autofill( @@ -33,4 +42,18 @@ fun Modifier.autofill( else -> autofill?.cancelAutofillForNode(autofillNode) } } -} \ No newline at end of file +} + +@SuppressLint("ComposableModifierFactory") +@Composable +fun Modifier.placeholder( + color: Color = MaterialTheme.bibLib.colors.placeHolder, + shimmer: Color = MaterialTheme.bibLib.colors.shimmer, + shape: Shape = CircleShape, + visible: () -> Boolean, +) = this.placeholder( + visible = visible(), + color = color, + shape = shape, + highlight = PlaceholderHighlight.shimmer(highlightColor = shimmer), +) \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 4e9608d..0850507 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -71,7 +71,7 @@ Trié par : %1$s Bonjour - Amails associés : + Emails associés : Aucun email associé Édition du profile Déconnexion diff --git a/build.gradle b/build.gradle index 97f8801..3d68bcf 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.3.1' + classpath 'com.android.tools.build:gradle:7.4.2' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10' classpath "com.google.dagger:hilt-android-gradle-plugin:2.43.2" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 856e2e1..c30efcb 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sat Oct 15 11:40:37 CEST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME