From 03671792d631053e56b8a692824b88bf11fe61de Mon Sep 17 00:00:00 2001 From: "Andres Gomez, Thomas (ITDV RL)" Date: Fri, 21 Jun 2024 19:17:52 +0200 Subject: [PATCH] Add a refresh feature for the adventure detail (was already flow <3) --- .../adventure/AdventureRepository.kt | 33 +++++++++++++++-- .../screens/ComposableAdventureDetail.kt | 16 ++++----- .../adventure/book/AdventureBooksScreen.kt | 7 +--- .../adventure/book/AdventureBooksViewModel.kt | 28 ++++++++------- .../adventure/chapter/AdventureChapter.kt | 2 ++ .../chapter/AdventureStoriesScreen.kt | 19 ++++++---- .../chapter/AdventureStoriesViewModel.kt | 35 +++++++++++-------- .../adventure/detail/AdventureDetailScreen.kt | 23 ++++++++++++ .../detail/AdventureDetailViewModel.kt | 33 +++++++++++++++-- .../screens/adventure/detail/AdventureLine.kt | 6 ++-- 10 files changed, 148 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/com/pixelized/rplexicon/data/repository/adventure/AdventureRepository.kt b/app/src/main/java/com/pixelized/rplexicon/data/repository/adventure/AdventureRepository.kt index 58ac45d..9814b2d 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/repository/adventure/AdventureRepository.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/repository/adventure/AdventureRepository.kt @@ -67,12 +67,12 @@ class AdventureRepository @Inject constructor( } fun adventureFlow( - bookTitle: String, + documentId: String, adventureTitle: String, ): Flow { return adventures.map { adventures -> adventures.firstOrNull { adventure -> - adventure.bookTitle == bookTitle && adventure.storyTitle == adventureTitle + adventure.documentId == documentId && adventure.storyTitle == adventureTitle } } } @@ -204,6 +204,35 @@ class AdventureRepository @Inject constructor( ) } + @Throws(IncompatibleSheetStructure::class, Exception::class) + suspend fun fetchAdventure( + documentId: String, + storyTitle: String, + ) { + val database = database.adventureDao() + + val lines = fetchAdventureLine( + documentId = documentId, + storyTitle = storyTitle, + ).mapIndexed { index, line -> + adventureDboFactory.convertToDbo( + adventureLine = line, + index = index, + documentId = documentId, + storyTitle = storyTitle, + ) + } + + database.update( + booksToRemove = emptyList(), + storiesToRemove = emptyList(), + linesToRemove = emptyList(), + books = emptyList(), + stories = emptyList(), + lines = lines, + ) + } + @Throws(IncompatibleSheetStructure::class, Exception::class) private suspend fun fetchAdventureBooks(): List { return googleRepository.fetch { sheet -> diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableAdventureDetail.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableAdventureDetail.kt index 9d6e79f..8d4750e 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableAdventureDetail.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableAdventureDetail.kt @@ -14,22 +14,22 @@ import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureDetailScreen import com.pixelized.rplexicon.utilitary.extentions.string.ARG private const val ROUTE = "adventureDetail" -private const val BOOK_TITLE_ARG = "bookTitle" +private const val DOCUMENT_ID_ARG = "documentId" private const val ADVENTURE_TITLE_ARG = "adventureTitle" -val ADVENTURE_DETAIL_ROUTE = "$ROUTE?${BOOK_TITLE_ARG.ARG}&${ADVENTURE_TITLE_ARG.ARG}" +val ADVENTURE_DETAIL_ROUTE = "$ROUTE?${DOCUMENT_ID_ARG.ARG}&${ADVENTURE_TITLE_ARG.ARG}" @Stable @Immutable class AdventureDetailArgument( - val bookTitle: String, + val documentId: String, val adventureTitle: String, ) val SavedStateHandle.adventureDetailArgument: AdventureDetailArgument get() = AdventureDetailArgument( - bookTitle = get(BOOK_TITLE_ARG) - ?: error("AdventureDetailArgument missing argument: $BOOK_TITLE_ARG"), + documentId = get(DOCUMENT_ID_ARG) + ?: error("AdventureDetailArgument missing argument: $DOCUMENT_ID_ARG"), adventureTitle = get(ADVENTURE_TITLE_ARG) ?: error("AdventureDetailArgument missing argument: $ADVENTURE_TITLE_ARG"), ) @@ -39,7 +39,7 @@ fun NavGraphBuilder.composableAdventureDetail() { route = ADVENTURE_DETAIL_ROUTE, animation = NavigationAnimation.Push, arguments = listOf( - navArgument(name = BOOK_TITLE_ARG) { + navArgument(name = DOCUMENT_ID_ARG) { type = NavType.StringType nullable = false }, @@ -54,10 +54,10 @@ fun NavGraphBuilder.composableAdventureDetail() { } fun NavHostController.navigateToAdventureDetail( - bookTitle: String, + documentId: String, adventureTitle: String, option: NavOptionsBuilder.() -> Unit = {}, ) { - val route = "$ROUTE?$BOOK_TITLE_ARG=$bookTitle&$ADVENTURE_TITLE_ARG=$adventureTitle" + val route = "$ROUTE?$DOCUMENT_ID_ARG=$documentId&$ADVENTURE_TITLE_ARG=$adventureTitle" navigate(route = route, builder = option) } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/book/AdventureBooksScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/book/AdventureBooksScreen.kt index de25257..fd4a7f9 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/book/AdventureBooksScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/book/AdventureBooksScreen.kt @@ -29,7 +29,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.shadow @@ -44,7 +43,6 @@ import com.pixelized.rplexicon.ui.composable.rememberAnimatedShadow import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost import com.pixelized.rplexicon.ui.navigation.screens.navigateToAdventureChapters import com.pixelized.rplexicon.ui.theme.LexiconTheme -import kotlinx.coroutines.launch @OptIn(ExperimentalMaterialApi::class) @Composable @@ -52,12 +50,9 @@ fun AdventureBooksScreen( viewModel: AdventureBooksViewModel = hiltViewModel(), ) { val screen = LocalScreenNavHost.current - val scope = rememberCoroutineScope() val refresh = rememberPullRefreshState( refreshing = false, - onRefresh = { - scope.launch { viewModel.update() } - }, + onRefresh = { viewModel.update() }, ) Surface( diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/book/AdventureBooksViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/book/AdventureBooksViewModel.kt index c33420b..1cc5130 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/book/AdventureBooksViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/book/AdventureBooksViewModel.kt @@ -56,19 +56,21 @@ class AdventureBooksViewModel @Inject constructor( } } - suspend fun update() { - try { - withContext(Dispatchers.Main) { - _isLoading.value = true - } - withContext(Dispatchers.Default) { - adventureRepository.fetchBooks() - } - } catch (_: Exception) { - _error.emit(value = Structure(ADVENTURE)) - } finally { - withContext(Dispatchers.Main) { - _isLoading.value = false + fun update() { + viewModelScope.launch { + try { + withContext(Dispatchers.Main) { + _isLoading.value = true + } + withContext(Dispatchers.Default) { + adventureRepository.fetchBooks() + } + } catch (_: Exception) { + _error.emit(value = Structure(ADVENTURE)) + } finally { + withContext(Dispatchers.Main) { + _isLoading.value = false + } } } } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/chapter/AdventureChapter.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/chapter/AdventureChapter.kt index 33a357e..50aad24 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/chapter/AdventureChapter.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/chapter/AdventureChapter.kt @@ -32,6 +32,7 @@ sealed class AdventureStoriesUio { @Stable data class AdventureItem( + val documentId: String, val bookTitle: String, val adventureTitle: String, ) : AdventureStoriesUio() @@ -110,6 +111,7 @@ private fun AdventureChapterItemPreview() { Surface { AdventureChapterItem( item = AdventureStoriesUio.AdventureItem( + documentId = "", bookTitle = "Les chroniques d'une orc", adventureTitle = "La traque", ), diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/chapter/AdventureStoriesScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/chapter/AdventureStoriesScreen.kt index ebfca94..075933b 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/chapter/AdventureStoriesScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/chapter/AdventureStoriesScreen.kt @@ -50,12 +50,9 @@ fun AdventureStoriesScreen( viewModel: AdventureStoriesViewModel = hiltViewModel(), ) { val screen = LocalScreenNavHost.current - val scope = rememberCoroutineScope() val refresh = rememberPullRefreshState( refreshing = false, - onRefresh = { - scope.launch { viewModel.update() } - }, + onRefresh = { viewModel.update() }, ) Surface( @@ -71,7 +68,7 @@ fun AdventureStoriesScreen( chapters = viewModel.chapters, onChapter = { screen.navigateToAdventureDetail( - bookTitle = it.bookTitle, + documentId = it.documentId, adventureTitle = it.adventureTitle ) }, @@ -118,7 +115,9 @@ private fun AdventureChapterContent( contentAlignment = Alignment.TopCenter, ) { LazyColumn( - modifier = Modifier.pullRefresh(state = refreshState), + modifier = Modifier + .fillMaxSize() + .pullRefresh(state = refreshState), state = lazyListState, contentPadding = paddingValues, verticalArrangement = Arrangement.spacedBy(space = 8.dp), @@ -176,6 +175,7 @@ private fun AdventureChapterPreview() { mutableStateOf( listOf( AdventureStoriesUio.AdventureItem( + documentId = "", bookTitle = "Les chroniques d'une orc", adventureTitle = "Biographie", ), @@ -183,30 +183,37 @@ private fun AdventureChapterPreview() { title = "Mémoire d'une orc", ), AdventureStoriesUio.AdventureItem( + documentId = "", bookTitle = "Les chroniques d'une orc", adventureTitle = "La traque", ), AdventureStoriesUio.AdventureItem( + documentId = "", bookTitle = "Les chroniques d'une orc", adventureTitle = "Les six mercenaires", ), AdventureStoriesUio.AdventureItem( + documentId = "", bookTitle = "Les chroniques d'une orc", adventureTitle = "La couronne de cuivre", ), AdventureStoriesUio.AdventureItem( + documentId = "", bookTitle = "Les chroniques d'une orc", adventureTitle = "Le seigneur tout puissant", ), AdventureStoriesUio.AdventureItem( + documentId = "", bookTitle = "Les chroniques d'une orc", adventureTitle = "Vague à l'Amn", ), AdventureStoriesUio.AdventureItem( + documentId = "", bookTitle = "Tu parles d'une galère", adventureTitle = "Tu parles d'une galère", ), AdventureStoriesUio.AdventureItem( + documentId = "", bookTitle = "Les chroniques d'une orc", adventureTitle = "Liberté", ), diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/chapter/AdventureStoriesViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/chapter/AdventureStoriesViewModel.kt index 01f304f..d4f8926 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/chapter/AdventureStoriesViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/chapter/AdventureStoriesViewModel.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.pixelized.rplexicon.data.repository.adventure.AdventureRepository import com.pixelized.rplexicon.ui.composable.error.FetchErrorUio import com.pixelized.rplexicon.ui.composable.error.FetchErrorUio.Structure @@ -17,6 +18,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject @@ -47,6 +49,7 @@ class AdventureStoriesViewModel @Inject constructor( ) val stories = entry.value.map { AdventureStoriesUio.AdventureItem( + documentId = it.documentId, bookTitle = it.bookTitle, adventureTitle = it.storyTitle, ) @@ -60,21 +63,23 @@ class AdventureStoriesViewModel @Inject constructor( @Stable get() = _chapters.collectAsState(initial = emptyList()) - suspend fun update() { - try { - withContext(Dispatchers.Main) { - _isLoading.value = true - } - withContext(Dispatchers.Default) { - adventureRepository.fetchStories( - documentId = argument.documentId, - ) - } - } catch (_: Exception) { - _error.emit(value = Structure(ADVENTURE)) - } finally { - withContext(Dispatchers.Main) { - _isLoading.value = false + fun update() { + viewModelScope.launch { + try { + withContext(Dispatchers.Main) { + _isLoading.value = true + } + withContext(Dispatchers.Default) { + adventureRepository.fetchStories( + documentId = argument.documentId, + ) + } + } catch (_: Exception) { + _error.emit(value = Structure(ADVENTURE)) + } finally { + withContext(Dispatchers.Main) { + _isLoading.value = false + } } } } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/detail/AdventureDetailScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/detail/AdventureDetailScreen.kt index fdb549d..eb47fdb 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/detail/AdventureDetailScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/detail/AdventureDetailScreen.kt @@ -3,6 +3,7 @@ package com.pixelized.rplexicon.ui.screens.adventure.detail import android.content.res.Configuration import android.net.Uri import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -15,6 +16,8 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -29,6 +32,7 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.mapSaver @@ -74,11 +78,13 @@ fun AdventureDetailScreen( modifier = Modifier .fillMaxSize() .navigationBarsPadding(), + isLoading = viewModel.isLoading, background = viewModel.background, title = viewModel.title, titleIndex = viewModel.titleIndex, adventures = viewModel.adventure, onBack = { screen.popBackStack() }, + onRefresh = { viewModel.update() }, ) } } @@ -100,11 +106,13 @@ private fun AdventureDetailContent( bottom = 16.dp, ), lazyListState: LazyListState = rememberLazyListState(), + isLoading: State, title: State, titleIndex: State, background: State, adventures: State>, onBack: () -> Unit, + onRefresh: () -> Unit, ) { val nestedScrollOffset = rememberSaveable { mutableFloatStateOf(0f) } val titleLayoutInfo = rememberSaveable(stateSaver = TitleLayoutInfoSaver) { @@ -183,6 +191,17 @@ private fun AdventureDetailContent( text = title.value ?: "", ) }, + actions = { + IconButton( + onClick = onRefresh, + enabled = isLoading.value.not(), + ) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = null, + ) + } + } ) LazyColumn( state = lazyListState, @@ -251,6 +270,9 @@ private fun AdventureDetailPreview() { LexiconTheme { Surface { AdventureDetailContent( + isLoading = remember { + mutableStateOf(false) + }, title = remember { mutableStateOf("La traque") }, @@ -321,6 +343,7 @@ private fun AdventureDetailPreview() { ) }, onBack = { }, + onRefresh = { }, ) } } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/detail/AdventureDetailViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/detail/AdventureDetailViewModel.kt index aa707b6..51b3567 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/detail/AdventureDetailViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/detail/AdventureDetailViewModel.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel @@ -14,20 +15,23 @@ import com.pixelized.rplexicon.data.repository.adventure.AdventureRepository import com.pixelized.rplexicon.ui.navigation.screens.adventureDetailArgument import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureLineUio.Style import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject @HiltViewModel class AdventureDetailViewModel @Inject constructor( - adventureRepository: AdventureRepository, + private val adventureRepository: AdventureRepository, savedStateHandle: SavedStateHandle, ) : ViewModel() { private val argument = savedStateHandle.adventureDetailArgument private val detail = adventureRepository.adventureFlow( - bookTitle = argument.bookTitle, + documentId = argument.documentId, adventureTitle = argument.adventureTitle, ).stateIn( scope = viewModelScope, @@ -35,6 +39,9 @@ class AdventureDetailViewModel @Inject constructor( initialValue = null, ) + private val _isLoading = mutableStateOf(false) + val isLoading: State get() = _isLoading + val background: State @Composable get() = remember(detail) { @@ -97,4 +104,26 @@ class AdventureDetailViewModel @Inject constructor( } } } + + fun update() { + viewModelScope.launch { + try { + withContext(Dispatchers.Main) { + _isLoading.value = true + } + withContext(Dispatchers.Default) { + adventureRepository.fetchAdventure( + documentId = argument.documentId, + storyTitle = argument.adventureTitle, + ) + } + } catch (_: Exception) { + //_error.emit(value = Structure(ADVENTURE)) + } finally { + withContext(Dispatchers.Main) { + _isLoading.value = false + } + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/detail/AdventureLine.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/detail/AdventureLine.kt index 9991e05..af20333 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/detail/AdventureLine.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/detail/AdventureLine.kt @@ -94,13 +94,15 @@ fun rememberPaddingValues( SUB_TITLE -> 0.dp CHAPTER -> when (previous?.style) { - TITLE -> 64.dp + TITLE -> 48.dp + SUB_TITLE -> 48.dp else -> 32.dp } PARAGRAPH -> when (previous?.style) { PARAGRAPH -> 8.dp - TITLE -> 64.dp + TITLE -> 48.dp + SUB_TITLE -> 48.dp else -> 16.dp }