diff --git a/app/src/main/java/com/pixelized/biblib/ui/MainActivity.kt b/app/src/main/java/com/pixelized/biblib/ui/MainActivity.kt index 31d30ae..c7eb9fa 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/MainActivity.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/MainActivity.kt @@ -7,17 +7,11 @@ import androidx.activity.viewModels import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.graphics.Color import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat -import com.bumptech.glide.load.engine.DiskCacheStrategy -import com.bumptech.glide.request.RequestOptions import com.pixelized.biblib.ui.composable.SystemThemeColor -import com.pixelized.biblib.ui.navigation.ScreenNavHost import com.pixelized.biblib.ui.screen.launch.LauncherViewModel import com.pixelized.biblib.ui.theme.BibLibTheme -import com.skydoves.landscapist.glide.LocalGlideRequestOptions import dagger.hilt.android.AndroidEntryPoint @@ -42,14 +36,9 @@ class MainActivity : ComponentActivity() { // Compose setContent { BibLibActivityTheme { - ProvideGlideOption { - // Handle the main Navigation - if (launcherViewModel.isLoadingDone) { - ScreenNavHost( - startDestination = launcherViewModel.startDestination - ) - } - } + MainContent( + launcherViewModel = launcherViewModel + ) } } } @@ -67,16 +56,4 @@ class MainActivity : ComponentActivity() { } } } - - @Composable - private fun ProvideGlideOption( - options: RequestOptions = RequestOptions().diskCacheStrategy(DiskCacheStrategy.ALL), - content: @Composable () -> Unit, - ) { - CompositionLocalProvider( - LocalGlideRequestOptions provides options, - ) { - content() - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/MainContent.kt b/app/src/main/java/com/pixelized/biblib/ui/MainContent.kt new file mode 100644 index 0000000..3246866 --- /dev/null +++ b/app/src/main/java/com/pixelized/biblib/ui/MainContent.kt @@ -0,0 +1,56 @@ +package com.pixelized.biblib.ui + +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.request.RequestOptions +import com.pixelized.biblib.ui.navigation.ScreenNavHost +import com.pixelized.biblib.ui.screen.launch.LauncherViewModel +import com.pixelized.biblib.utils.extention.bibLib +import com.skydoves.landscapist.glide.LocalGlideRequestOptions + +val LocalSnackHostState = staticCompositionLocalOf { + error("SnackBarHostState is not ready yet.") +} + +@Composable +fun MainContent( + launcherViewModel: LauncherViewModel = hiltViewModel(), + glideOptions: RequestOptions = RequestOptions().diskCacheStrategy(DiskCacheStrategy.ALL) +) { + val scaffoldState: ScaffoldState = rememberScaffoldState() + + CompositionLocalProvider( + LocalGlideRequestOptions provides glideOptions, + LocalSnackHostState provides scaffoldState.snackbarHostState, + ) { + Scaffold( + scaffoldState = scaffoldState, + snackbarHost = { snackHostState -> + SnackbarHost( + modifier = Modifier.systemBarsPadding(), + hostState = snackHostState, + ) { snackBarData -> + Snackbar( + snackbarData = snackBarData, + backgroundColor = MaterialTheme.colors.error, + contentColor = MaterialTheme.colors.onError, + actionColor = MaterialTheme.colors.onError, + ) + } + }, + content = { + if (launcherViewModel.isLoadingDone) { + ScreenNavHost( + startDestination = launcherViewModel.startDestination + ) + } + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/BookDetailViewModel.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/BookDetailViewModel.kt index 4504e27..974f550 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/BookDetailViewModel.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/BookDetailViewModel.kt @@ -1,18 +1,19 @@ package com.pixelized.biblib.ui.screen.home.detail +import android.app.Application import android.util.Log import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf -import androidx.lifecycle.ViewModel +import androidx.lifecycle.AndroidViewModel 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.ui.screen.home.detail.BookDetailUioErrorUio.Type +import com.pixelized.biblib.utils.extention.stringResource import com.pixelized.biblib.utils.extention.toDetailUio import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch @@ -20,37 +21,41 @@ import javax.inject.Inject @HiltViewModel class BookDetailViewModel @Inject constructor( + application: Application, private val bookRepository: BookRepository, private val client: IBibLibClient, -) : ViewModel() { +) : AndroidViewModel(application) { + + private val _detail = mutableStateOf(null) + val detail: State get() = _detail + + private val _sendStatus = MutableSharedFlow() + val sendStatus: Flow get() = _sendStatus private val _error = MutableSharedFlow() val error: Flow get() = _error - fun getDetail(id: Int?): State { - return mutableStateOf(null).apply { - viewModelScope.launch(Dispatchers.IO) { - try { - requireNotNull(id) - value = getCacheBookDetail(id = id) - value = getBookDetail(id = id) - } catch (exception: Exception) { - _error.emit(exception.toUio(Type.GET_DETAIL)) - } + fun getDetail(id: Int) { + viewModelScope.launch(Dispatchers.IO) { + try { + _detail.value = getCacheBookDetail(id = id) + _detail.value = getBookDetail(id = id) + } catch (exception: Exception) { + _error.emit(toDetailErrorUio(bookId = id)) } } } - suspend fun send(bookId: Int, email: String): State { - return mutableStateOf(null).apply { - viewModelScope.launch(Dispatchers.IO) { - try { - val data = client.service.send(bookId = bookId, mail = email) - Log.d("send", data.toString()) - } catch (exception: Exception) { - Log.d("send", exception.message, exception) - _error.emit(exception.toUio(Type.SEND_BOOK)) - } + suspend fun send(bookId: Int, email: String) { + viewModelScope.launch(Dispatchers.IO) { + try { + val data = client.service.send(bookId = bookId, mail = email) + _sendStatus.emit(true) + Log.d("send", data.toString()) + } catch (exception: Exception) { + Log.d("send", exception.message, exception) + _sendStatus.emit(false) + _error.emit(toSendBookUio(bookId = bookId, mail = email)) } } } @@ -66,8 +71,16 @@ class BookDetailViewModel @Inject constructor( return book.toDetailUio() } - private fun Exception.toUio(type: Type) = BookDetailUioErrorUio( - type = type, - message = this.message ?: "An error occurred." + private fun toDetailErrorUio(bookId: Int) = BookDetailUioErrorUio.GetDetailInput( + message = stringResource(R.string.error_get_book_detail_message), + action = stringResource(R.string.error_get_book_detail_action), + bookId = bookId, + ) + + private fun toSendBookUio(bookId: Int, mail: String) = BookDetailUioErrorUio.SendBookInput( + message = stringResource(R.string.error_send_book_message), + action = stringResource(R.string.error_send_book_action), + bookId = bookId, + mail = mail, ) } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/DetailScreen.kt b/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/DetailScreen.kt index 56bb6f3..99d2de2 100644 --- a/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/DetailScreen.kt +++ b/app/src/main/java/com/pixelized/biblib/ui/screen/home/detail/DetailScreen.kt @@ -6,12 +6,12 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext 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.isSuccessful import com.pixelized.biblib.ui.scaffold.LocalDetailBottomSheetState import com.pixelized.biblib.ui.screen.home.page.profile.ProfileViewModel @@ -51,14 +51,26 @@ data class BookDetailUio( @Stable @Immutable -data class BookDetailUioErrorUio( - val type: Type, +sealed class BookDetailUioErrorUio( val message: String, + val action: String?, ) { - enum class Type { - GET_DETAIL, - SEND_BOOK, - } + @Stable + @Immutable + class GetDetailInput( + message: String, + action: String?, + val bookId: Int, + ) : BookDetailUioErrorUio(message, action) + + @Stable + @Immutable + class SendBookInput( + message: String, + action: String?, + val bookId: Int, + val mail: String, + ) : BookDetailUioErrorUio(message, action) } @OptIn(ExperimentalMaterialApi::class) @@ -70,6 +82,7 @@ fun DetailScreen( bookId: Int? = null, ) { val detailState = LocalDetailBottomSheetState.current + val snackBarHost = LocalSnackHostState.current val uriHandler = LocalUriHandler.current val context = LocalContext.current @@ -79,11 +92,7 @@ fun DetailScreen( skipHalfExpanded = true, ) - val bookDetailState: State = rememberSaveable(bookId) { - detailViewModel.getDetail(bookId) - } - - when (val detail = bookDetailState.value) { + when (val detail = detailViewModel.detail.value) { null -> EmptyDetail() else -> { ModalBottomSheetLayout( @@ -155,16 +164,36 @@ fun DetailScreen( } } - LaunchedEffect(key1 = "DetailScreenError") { - detailViewModel.error.collect { - context.showToast(message = it.message) + if (detailState.bottomSheetState.isVisible || detailState.bottomSheetState.isAnimationRunning) { + LaunchedEffect(key1 = "DetailScreenError") { + detailViewModel.error.collect { + val result = snackBarHost.showSnackbar( + message = it.message, + actionLabel = it.action, + duration = SnackbarDuration.Indefinite, + ) + if (result == SnackbarResult.ActionPerformed) { + when (it) { + is BookDetailUioErrorUio.GetDetailInput -> { + detailViewModel.getDetail(id = it.bookId) + } + is BookDetailUioErrorUio.SendBookInput -> { + detailViewModel.send(bookId = it.bookId, email = it.mail) + } + } + } + } + } + + LaunchedEffect(key1 = bookId) { + bookId?.let { detailViewModel.getDetail(it) } } } } @Composable private fun EmptyDetail( - modifier : Modifier = Modifier, + modifier: Modifier = Modifier, ) = Box( modifier = modifier.fillMaxSize() ) \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0e2e7c0..b00a13f 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -14,6 +14,14 @@ Auteurs Genres + + + Réessayer + La récupération des détails a échouée. + @string/error_action_retry + L\'envoi de l\'eBook a échoué. + @string/error_action_retry + Oups! diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cb83c28..b26a567 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,6 +20,14 @@ Authors Genres + + + Retry + Failed to retrieve book details. + @string/error_action_retry + Failed to send the book to your kindle. + @string/error_action_retry + Oops!