Update plugin, & architecture clean up.

This commit is contained in:
Thomas Andres Gomez 2023-03-27 13:09:20 +02:00
parent d17912207a
commit 8be1ecc0a7
32 changed files with 486 additions and 681 deletions

View file

@ -82,6 +82,7 @@ android {
lint {
disable 'MissingTranslation'
}
namespace 'com.pixelized.biblib'
}
dependencies {

View file

@ -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" />

View file

@ -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,
)
}
}

View file

@ -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())
}
}

View file

@ -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,
)
}
}

View file

@ -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,

View file

@ -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>> {

View file

@ -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 {

View file

@ -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 {

View file

@ -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>()

View file

@ -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
}
}
}

View file

@ -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>
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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
}

View file

@ -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"
}
}

View file

@ -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.
}
}

View file

@ -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())
}
}

View file

@ -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)
)
}
}

View file

@ -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))
}
}

View file

@ -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,

View file

@ -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")
}
}
}
}

View file

@ -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<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(
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 ""
}

View file

@ -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())

View file

@ -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(

View file

@ -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()
}
}

View file

@ -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<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,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<UserUio?>
) {
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<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",
amazonEmails = listOf(
"r.daneel.olivaw@robot.com",
"r.daneel.olivaw@biblib.com",
),
)
),
)
}

View file

@ -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) {
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
}
}

View file

@ -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)
}
}
}
}
@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),
)

View file

@ -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>

View file

@ -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"
}

View file

@ -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