Update plugin, & architecture clean up.
This commit is contained in:
parent
d17912207a
commit
8be1ecc0a7
32 changed files with 486 additions and 681 deletions
|
|
@ -82,6 +82,7 @@ android {
|
|||
lint {
|
||||
disable 'MissingTranslation'
|
||||
}
|
||||
namespace 'com.pixelized.biblib'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.pixelized.biblib">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<Int, List<Genre>> {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import com.pixelized.biblib.utils.exception.missingField
|
|||
typealias SeriesHash = Map<Int, Pair<Int, Series>>
|
||||
|
||||
class SeriesFactory {
|
||||
|
||||
fun fromListResponseToSeriesHash(
|
||||
response: SeriesListResponse,
|
||||
): SeriesHash {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<Book> =
|
||||
|
|
@ -23,25 +39,110 @@ class BookRepository @Inject constructor(
|
|||
override fun getBookCount(): Int =
|
||||
database.bookDao().count()
|
||||
|
||||
override fun getNewsSource(): DataSource.Factory<Int, Book> =
|
||||
database.bookDao().getNews().map { it.toBook() }
|
||||
|
||||
override fun getBooksSource(): DataSource.Factory<Int, Book> =
|
||||
database.bookDao().getBook().map { it.toBook() }
|
||||
|
||||
override fun getAuthorsSource(): DataSource.Factory<Int, Author> =
|
||||
database.authorDao().getAll().map { it.toAuthor() }
|
||||
override fun getNewsSource(): DataSource.Factory<Int, Book> =
|
||||
database.bookDao().getNews().map { it.toBook() }
|
||||
|
||||
override fun getSeriesSource(): DataSource.Factory<Int, Series> =
|
||||
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<Int, Genre> =
|
||||
database.genreDao().getAll().map { it.toGenre() }
|
||||
override suspend fun update() = withContext(Dispatchers.IO) {
|
||||
if (getBookCount() <= 0 || fetchNewBooks()) {
|
||||
fetchAllBooks()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLanguagesSource(): DataSource.Factory<Int, Language> =
|
||||
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<Book>) {
|
||||
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<Book>) {
|
||||
val authors = mutableSetOf<AuthorDbo>()
|
||||
val genres = mutableSetOf<GenreDbo>()
|
||||
val series = mutableSetOf<SeriesDbo>()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,23 +5,17 @@ import com.pixelized.biblib.model.book.*
|
|||
|
||||
interface IBookRepository {
|
||||
|
||||
suspend fun update()
|
||||
|
||||
suspend fun getBookDetail(id: Int): Book
|
||||
|
||||
fun getAll(): List<Book>
|
||||
|
||||
fun getBook(id: Int): Book
|
||||
|
||||
fun getBookCount(): Int
|
||||
|
||||
fun getNewsSource(): DataSource.Factory<Int, Book>
|
||||
|
||||
fun getBooksSource(): DataSource.Factory<Int, Book>
|
||||
|
||||
fun getAuthorsSource() : DataSource.Factory<Int, Author>
|
||||
|
||||
fun getSeriesSource(): DataSource.Factory<Int, Series>
|
||||
|
||||
fun getGenresSource(): DataSource.Factory<Int, Genre>
|
||||
|
||||
fun getLanguagesSource(): DataSource.Factory<Int, Language>
|
||||
|
||||
suspend fun update(data: List<Book>)
|
||||
fun getNewsSource(): DataSource.Factory<Int, Book>
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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<String>
|
||||
suspend fun user(forceUpdate: Boolean = false): User
|
||||
suspend fun lastUpdateDuration(): Long
|
||||
}
|
||||
|
|
@ -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<String> {
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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<T> {
|
||||
class Progress<T>(val progress: Float? = null, val cache : T? = null) : StateUio<T>()
|
||||
class Failure<T>(val exception: Exception, val cache: T? = null) : StateUio<T>()
|
||||
class Success<T>(val value: T) : StateUio<T>()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
fun <T> StateUio<T>.isSuccessful(): Boolean {
|
||||
contract {
|
||||
returns(true) implies (this@isSuccessful is StateUio.Success<T>)
|
||||
}
|
||||
return this is StateUio.Success<T>
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun <T> StateUioHandler(
|
||||
state: StateUio<T>?,
|
||||
onDismissRequest: (StateUio<T>) -> 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.
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -247,46 +247,68 @@ private fun CredentialRemember(
|
|||
}
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
private fun googleStringResource(): AnnotatedString = buildAnnotatedString {
|
||||
private fun googleStringResource(): AnnotatedString {
|
||||
val default = LocalTextStyle.current.toSpanStyle()
|
||||
val google = stringResource(id = R.string.action_google_sign_in)
|
||||
return remember {
|
||||
buildAnnotatedString {
|
||||
withStyle(
|
||||
style = default
|
||||
) {
|
||||
append(stringResource(id = R.string.action_google_sign_in))
|
||||
append(google)
|
||||
append(" ")
|
||||
}
|
||||
withStyle(
|
||||
style = default.copy(color = GoogleColorPalette.blue, fontWeight = FontWeight.ExtraBold),
|
||||
style = default.copy(
|
||||
color = GoogleColorPalette.blue,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
),
|
||||
) {
|
||||
append("G")
|
||||
}
|
||||
withStyle(
|
||||
style = default.copy(color = GoogleColorPalette.red, fontWeight = FontWeight.ExtraBold),
|
||||
style = default.copy(
|
||||
color = GoogleColorPalette.red,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
),
|
||||
) {
|
||||
append("o")
|
||||
}
|
||||
withStyle(
|
||||
style = default.copy(color = GoogleColorPalette.yellow, fontWeight = FontWeight.ExtraBold),
|
||||
style = default.copy(
|
||||
color = GoogleColorPalette.yellow,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
),
|
||||
) {
|
||||
append("o")
|
||||
}
|
||||
withStyle(
|
||||
style = default.copy(color = GoogleColorPalette.blue, fontWeight = FontWeight.ExtraBold),
|
||||
style = default.copy(
|
||||
color = GoogleColorPalette.blue,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
),
|
||||
) {
|
||||
append("g")
|
||||
}
|
||||
withStyle(
|
||||
style = default.copy(color = GoogleColorPalette.green, fontWeight = FontWeight.ExtraBold),
|
||||
style = default.copy(
|
||||
color = GoogleColorPalette.green,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
),
|
||||
) {
|
||||
append("l")
|
||||
}
|
||||
withStyle(
|
||||
style = default.copy(color = GoogleColorPalette.red, fontWeight = FontWeight.ExtraBold),
|
||||
style = default.copy(
|
||||
color = GoogleColorPalette.red,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
),
|
||||
) {
|
||||
append("e")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||
|
|
|
|||
|
|
@ -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,27 +12,16 @@ class AuthenticationFormViewModel @Inject constructor(
|
|||
private val credentialRepository: ICredentialRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _login: MutableState<String>
|
||||
private val _password: MutableState<String>
|
||||
private val _login = mutableStateOf(credentialRepository.rememberedLogin)
|
||||
private val _password = mutableStateOf(credentialRepository.rememberedPassword)
|
||||
private val _remember = mutableStateOf(credentialRepository.rememberCredential)
|
||||
|
||||
val form: AuthenticationFormUIO
|
||||
get() = AuthenticationFormUIO(
|
||||
val form: AuthenticationFormUIO = AuthenticationFormUIO(
|
||||
login = _login,
|
||||
password = _password,
|
||||
remember = _remember,
|
||||
)
|
||||
|
||||
init {
|
||||
if (credentialRepository.rememberCredential) {
|
||||
_login = mutableStateOf(credentialRepository.login ?: "")
|
||||
_password = mutableStateOf(credentialRepository.password ?: "")
|
||||
} else {
|
||||
_login = mutableStateOf("")
|
||||
_password = mutableStateOf("")
|
||||
}
|
||||
}
|
||||
|
||||
fun onLoginChange(login: String) {
|
||||
// update login in the repository
|
||||
if (_remember.value) {
|
||||
|
|
@ -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 ""
|
||||
}
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<UserUio>,
|
||||
user: State<UserUio?>,
|
||||
) {
|
||||
val user = userState as? StateUio.Success<UserUio>
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,13 +35,11 @@ fun ProfilePage(
|
|||
val navigation = LocalScreenNavHostController.current
|
||||
|
||||
Card {
|
||||
when (val user = viewModel.user) {
|
||||
is StateUio.Progress -> Unit
|
||||
is StateUio.Success -> ProfileScreenContent(
|
||||
ProfileScreenContent(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(MaterialTheme.bibLib.dimen.dp16),
|
||||
user = user.value,
|
||||
user = viewModel.user,
|
||||
onEditClick = {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(IBibLibClient.EDIT_PROFILE))
|
||||
context.startActivity(intent)
|
||||
|
|
@ -50,19 +49,24 @@ fun ProfilePage(
|
|||
navigation.navigateToAuthentication()
|
||||
}
|
||||
)
|
||||
is StateUio.Failure -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileScreenContent(
|
||||
modifier: Modifier = Modifier,
|
||||
user: UserUio,
|
||||
user: State<UserUio?>,
|
||||
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,30 +74,29 @@ 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 {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.padding(end = MaterialTheme.bibLib.dimen.dp4),
|
||||
modifier = Modifier.placeholder { placeholder },
|
||||
style = MaterialTheme.typography.body1,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
text = it,
|
||||
text = user.value?.firstname ?: if (placeholder) "R. Daneel" else "",
|
||||
)
|
||||
}
|
||||
user.lastname?.let {
|
||||
|
||||
Text(
|
||||
modifier = Modifier.placeholder { placeholder },
|
||||
style = MaterialTheme.typography.body1,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
text = it,
|
||||
text = user.value?.lastname ?: if (placeholder) "Olivaw" else "",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
modifier = Modifier.padding(top = MaterialTheme.bibLib.dimen.dp16),
|
||||
|
|
@ -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,30 +116,35 @@ 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(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Button(
|
||||
colors = ButtonDefaults.textButtonColors(),
|
||||
onClick = onEditClick,
|
||||
) {
|
||||
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(
|
||||
modifier = Modifier
|
||||
.align(Alignment.End),
|
||||
colors = ButtonDefaults.outlinedButtonColors(),
|
||||
colors = ButtonDefaults.textButtonColors(),
|
||||
onClick = onLogoutClick,
|
||||
) {
|
||||
Text(
|
||||
|
|
@ -152,18 +153,39 @@ 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<UserUio?>
|
||||
) {
|
||||
BibLibTheme {
|
||||
Box {
|
||||
ProfileScreenContent(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(all = MaterialTheme.bibLib.dimen.dp16),
|
||||
user = UserUio(
|
||||
user = preview,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ProfilePreviewProvider : PreviewParameterProvider<State<UserUio?>> {
|
||||
override val values: Sequence<State<UserUio?>> = sequenceOf(
|
||||
mutableStateOf(null),
|
||||
mutableStateOf(
|
||||
UserUio(
|
||||
username = "DefinitelyNotARobot",
|
||||
firstname = null,
|
||||
lastname = null,
|
||||
amazonEmails = emptyList(),
|
||||
)
|
||||
),
|
||||
mutableStateOf(
|
||||
UserUio(
|
||||
username = "DefinitelyNotARobot",
|
||||
firstname = "R. Daneel",
|
||||
lastname = "Olivaw",
|
||||
|
|
@ -171,8 +193,7 @@ private fun ProfileScreenContentPreview() {
|
|||
"r.daneel.olivaw@robot.com",
|
||||
"r.daneel.olivaw@biblib.com",
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UserUio>>(StateUio.Progress())
|
||||
private set
|
||||
private val _user = mutableStateOf<UserUio?>(null)
|
||||
val user: State<UserUio?> = _user
|
||||
|
||||
val mails: List<String> by derivedStateOf {
|
||||
(user as? StateUio.Success<UserUio>)?.value?.amazonEmails ?: emptyList()
|
||||
}
|
||||
private val _error = MutableSharedFlow<Exception>()
|
||||
val error: Flow<Exception> = _error
|
||||
|
||||
val mails: List<String> by derivedStateOf { user.value?.amazonEmails ?: emptyList() }
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
|
|
@ -39,19 +39,15 @@ class ProfileViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun updateUser(): StateUio<UserUio> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
suspend fun updateUser() {
|
||||
try {
|
||||
val factory = UserFactory()
|
||||
val response = client.service.user()
|
||||
val data = factory.fromUserResponseToUser(response)
|
||||
StateUio.Success(data.toUio())
|
||||
val data = userRepository.user(
|
||||
forceUpdate = userRepository.lastUpdateDuration() > DAY_IN_MILLIS
|
||||
)
|
||||
_user.value = data.toUio()
|
||||
} catch (exception: Exception) {
|
||||
Log.e("AccountViewModel", exception.message, exception)
|
||||
StateUio.Failure(exception)
|
||||
}.also {
|
||||
user = it
|
||||
}
|
||||
_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
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
@ -34,3 +43,17 @@ fun Modifier.autofill(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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),
|
||||
)
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
<string name="search_sort_by">Trié par : %1$s</string>
|
||||
|
||||
<string name="profile_title">Bonjour</string>
|
||||
<string name="profile_emails">Amails associés :</string>
|
||||
<string name="profile_emails">Emails associés :</string>
|
||||
<string name="profile_emails_empty">Aucun email associé</string>
|
||||
<string name="profile_edit_action">Édition du profile</string>
|
||||
<string name="profile_logout_action">Déconnexion</string>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue