Add a refresh feature for the adventure detail (was already flow <3)

This commit is contained in:
Andres Gomez, Thomas (ITDV RL) 2024-06-21 19:17:52 +02:00
parent 5a1cedf4ff
commit 03671792d6
10 changed files with 148 additions and 54 deletions

View file

@ -67,12 +67,12 @@ class AdventureRepository @Inject constructor(
} }
fun adventureFlow( fun adventureFlow(
bookTitle: String, documentId: String,
adventureTitle: String, adventureTitle: String,
): Flow<Adventure?> { ): Flow<Adventure?> {
return adventures.map { adventures -> return adventures.map { adventures ->
adventures.firstOrNull { adventure -> 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) @Throws(IncompatibleSheetStructure::class, Exception::class)
private suspend fun fetchAdventureBooks(): List<AdventureBook> { private suspend fun fetchAdventureBooks(): List<AdventureBook> {
return googleRepository.fetch { sheet -> return googleRepository.fetch { sheet ->

View file

@ -14,22 +14,22 @@ import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureDetailScreen
import com.pixelized.rplexicon.utilitary.extentions.string.ARG import com.pixelized.rplexicon.utilitary.extentions.string.ARG
private const val ROUTE = "adventureDetail" 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" 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 @Stable
@Immutable @Immutable
class AdventureDetailArgument( class AdventureDetailArgument(
val bookTitle: String, val documentId: String,
val adventureTitle: String, val adventureTitle: String,
) )
val SavedStateHandle.adventureDetailArgument: AdventureDetailArgument val SavedStateHandle.adventureDetailArgument: AdventureDetailArgument
get() = AdventureDetailArgument( get() = AdventureDetailArgument(
bookTitle = get(BOOK_TITLE_ARG) documentId = get(DOCUMENT_ID_ARG)
?: error("AdventureDetailArgument missing argument: $BOOK_TITLE_ARG"), ?: error("AdventureDetailArgument missing argument: $DOCUMENT_ID_ARG"),
adventureTitle = get(ADVENTURE_TITLE_ARG) adventureTitle = get(ADVENTURE_TITLE_ARG)
?: error("AdventureDetailArgument missing argument: $ADVENTURE_TITLE_ARG"), ?: error("AdventureDetailArgument missing argument: $ADVENTURE_TITLE_ARG"),
) )
@ -39,7 +39,7 @@ fun NavGraphBuilder.composableAdventureDetail() {
route = ADVENTURE_DETAIL_ROUTE, route = ADVENTURE_DETAIL_ROUTE,
animation = NavigationAnimation.Push, animation = NavigationAnimation.Push,
arguments = listOf( arguments = listOf(
navArgument(name = BOOK_TITLE_ARG) { navArgument(name = DOCUMENT_ID_ARG) {
type = NavType.StringType type = NavType.StringType
nullable = false nullable = false
}, },
@ -54,10 +54,10 @@ fun NavGraphBuilder.composableAdventureDetail() {
} }
fun NavHostController.navigateToAdventureDetail( fun NavHostController.navigateToAdventureDetail(
bookTitle: String, documentId: String,
adventureTitle: String, adventureTitle: String,
option: NavOptionsBuilder.() -> Unit = {}, 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) navigate(route = route, builder = option)
} }

View file

@ -29,7 +29,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow 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.LocalScreenNavHost
import com.pixelized.rplexicon.ui.navigation.screens.navigateToAdventureChapters import com.pixelized.rplexicon.ui.navigation.screens.navigateToAdventureChapters
import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.ui.theme.LexiconTheme
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@ -52,12 +50,9 @@ fun AdventureBooksScreen(
viewModel: AdventureBooksViewModel = hiltViewModel(), viewModel: AdventureBooksViewModel = hiltViewModel(),
) { ) {
val screen = LocalScreenNavHost.current val screen = LocalScreenNavHost.current
val scope = rememberCoroutineScope()
val refresh = rememberPullRefreshState( val refresh = rememberPullRefreshState(
refreshing = false, refreshing = false,
onRefresh = { onRefresh = { viewModel.update() },
scope.launch { viewModel.update() }
},
) )
Surface( Surface(

View file

@ -56,7 +56,8 @@ class AdventureBooksViewModel @Inject constructor(
} }
} }
suspend fun update() { fun update() {
viewModelScope.launch {
try { try {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
_isLoading.value = true _isLoading.value = true
@ -72,4 +73,5 @@ class AdventureBooksViewModel @Inject constructor(
} }
} }
} }
}
} }

View file

@ -32,6 +32,7 @@ sealed class AdventureStoriesUio {
@Stable @Stable
data class AdventureItem( data class AdventureItem(
val documentId: String,
val bookTitle: String, val bookTitle: String,
val adventureTitle: String, val adventureTitle: String,
) : AdventureStoriesUio() ) : AdventureStoriesUio()
@ -110,6 +111,7 @@ private fun AdventureChapterItemPreview() {
Surface { Surface {
AdventureChapterItem( AdventureChapterItem(
item = AdventureStoriesUio.AdventureItem( item = AdventureStoriesUio.AdventureItem(
documentId = "",
bookTitle = "Les chroniques d'une orc", bookTitle = "Les chroniques d'une orc",
adventureTitle = "La traque", adventureTitle = "La traque",
), ),

View file

@ -50,12 +50,9 @@ fun AdventureStoriesScreen(
viewModel: AdventureStoriesViewModel = hiltViewModel(), viewModel: AdventureStoriesViewModel = hiltViewModel(),
) { ) {
val screen = LocalScreenNavHost.current val screen = LocalScreenNavHost.current
val scope = rememberCoroutineScope()
val refresh = rememberPullRefreshState( val refresh = rememberPullRefreshState(
refreshing = false, refreshing = false,
onRefresh = { onRefresh = { viewModel.update() },
scope.launch { viewModel.update() }
},
) )
Surface( Surface(
@ -71,7 +68,7 @@ fun AdventureStoriesScreen(
chapters = viewModel.chapters, chapters = viewModel.chapters,
onChapter = { onChapter = {
screen.navigateToAdventureDetail( screen.navigateToAdventureDetail(
bookTitle = it.bookTitle, documentId = it.documentId,
adventureTitle = it.adventureTitle adventureTitle = it.adventureTitle
) )
}, },
@ -118,7 +115,9 @@ private fun AdventureChapterContent(
contentAlignment = Alignment.TopCenter, contentAlignment = Alignment.TopCenter,
) { ) {
LazyColumn( LazyColumn(
modifier = Modifier.pullRefresh(state = refreshState), modifier = Modifier
.fillMaxSize()
.pullRefresh(state = refreshState),
state = lazyListState, state = lazyListState,
contentPadding = paddingValues, contentPadding = paddingValues,
verticalArrangement = Arrangement.spacedBy(space = 8.dp), verticalArrangement = Arrangement.spacedBy(space = 8.dp),
@ -176,6 +175,7 @@ private fun AdventureChapterPreview() {
mutableStateOf( mutableStateOf(
listOf( listOf(
AdventureStoriesUio.AdventureItem( AdventureStoriesUio.AdventureItem(
documentId = "",
bookTitle = "Les chroniques d'une orc", bookTitle = "Les chroniques d'une orc",
adventureTitle = "Biographie", adventureTitle = "Biographie",
), ),
@ -183,30 +183,37 @@ private fun AdventureChapterPreview() {
title = "Mémoire d'une orc", title = "Mémoire d'une orc",
), ),
AdventureStoriesUio.AdventureItem( AdventureStoriesUio.AdventureItem(
documentId = "",
bookTitle = "Les chroniques d'une orc", bookTitle = "Les chroniques d'une orc",
adventureTitle = "La traque", adventureTitle = "La traque",
), ),
AdventureStoriesUio.AdventureItem( AdventureStoriesUio.AdventureItem(
documentId = "",
bookTitle = "Les chroniques d'une orc", bookTitle = "Les chroniques d'une orc",
adventureTitle = "Les six mercenaires", adventureTitle = "Les six mercenaires",
), ),
AdventureStoriesUio.AdventureItem( AdventureStoriesUio.AdventureItem(
documentId = "",
bookTitle = "Les chroniques d'une orc", bookTitle = "Les chroniques d'une orc",
adventureTitle = "La couronne de cuivre", adventureTitle = "La couronne de cuivre",
), ),
AdventureStoriesUio.AdventureItem( AdventureStoriesUio.AdventureItem(
documentId = "",
bookTitle = "Les chroniques d'une orc", bookTitle = "Les chroniques d'une orc",
adventureTitle = "Le seigneur tout puissant", adventureTitle = "Le seigneur tout puissant",
), ),
AdventureStoriesUio.AdventureItem( AdventureStoriesUio.AdventureItem(
documentId = "",
bookTitle = "Les chroniques d'une orc", bookTitle = "Les chroniques d'une orc",
adventureTitle = "Vague à l'Amn", adventureTitle = "Vague à l'Amn",
), ),
AdventureStoriesUio.AdventureItem( AdventureStoriesUio.AdventureItem(
documentId = "",
bookTitle = "Tu parles d'une galère", bookTitle = "Tu parles d'une galère",
adventureTitle = "Tu parles d'une galère", adventureTitle = "Tu parles d'une galère",
), ),
AdventureStoriesUio.AdventureItem( AdventureStoriesUio.AdventureItem(
documentId = "",
bookTitle = "Les chroniques d'une orc", bookTitle = "Les chroniques d'une orc",
adventureTitle = "Liberté", adventureTitle = "Liberté",
), ),

View file

@ -8,6 +8,7 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.rplexicon.data.repository.adventure.AdventureRepository import com.pixelized.rplexicon.data.repository.adventure.AdventureRepository
import com.pixelized.rplexicon.ui.composable.error.FetchErrorUio import com.pixelized.rplexicon.ui.composable.error.FetchErrorUio
import com.pixelized.rplexicon.ui.composable.error.FetchErrorUio.Structure 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.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@ -47,6 +49,7 @@ class AdventureStoriesViewModel @Inject constructor(
) )
val stories = entry.value.map { val stories = entry.value.map {
AdventureStoriesUio.AdventureItem( AdventureStoriesUio.AdventureItem(
documentId = it.documentId,
bookTitle = it.bookTitle, bookTitle = it.bookTitle,
adventureTitle = it.storyTitle, adventureTitle = it.storyTitle,
) )
@ -60,7 +63,8 @@ class AdventureStoriesViewModel @Inject constructor(
@Stable @Stable
get() = _chapters.collectAsState(initial = emptyList()) get() = _chapters.collectAsState(initial = emptyList())
suspend fun update() { fun update() {
viewModelScope.launch {
try { try {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
_isLoading.value = true _isLoading.value = true
@ -78,4 +82,5 @@ class AdventureStoriesViewModel @Inject constructor(
} }
} }
} }
}
} }

View file

@ -3,6 +3,7 @@ package com.pixelized.rplexicon.ui.screens.adventure.detail
import android.content.res.Configuration import android.content.res.Configuration
import android.net.Uri import android.net.Uri
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues 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.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState 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.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -29,6 +32,7 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.mapSaver import androidx.compose.runtime.saveable.mapSaver
@ -74,11 +78,13 @@ fun AdventureDetailScreen(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.navigationBarsPadding(), .navigationBarsPadding(),
isLoading = viewModel.isLoading,
background = viewModel.background, background = viewModel.background,
title = viewModel.title, title = viewModel.title,
titleIndex = viewModel.titleIndex, titleIndex = viewModel.titleIndex,
adventures = viewModel.adventure, adventures = viewModel.adventure,
onBack = { screen.popBackStack() }, onBack = { screen.popBackStack() },
onRefresh = { viewModel.update() },
) )
} }
} }
@ -100,11 +106,13 @@ private fun AdventureDetailContent(
bottom = 16.dp, bottom = 16.dp,
), ),
lazyListState: LazyListState = rememberLazyListState(), lazyListState: LazyListState = rememberLazyListState(),
isLoading: State<Boolean>,
title: State<String?>, title: State<String?>,
titleIndex: State<Int>, titleIndex: State<Int>,
background: State<Uri?>, background: State<Uri?>,
adventures: State<List<AdventureLineUio>>, adventures: State<List<AdventureLineUio>>,
onBack: () -> Unit, onBack: () -> Unit,
onRefresh: () -> Unit,
) { ) {
val nestedScrollOffset = rememberSaveable { mutableFloatStateOf(0f) } val nestedScrollOffset = rememberSaveable { mutableFloatStateOf(0f) }
val titleLayoutInfo = rememberSaveable(stateSaver = TitleLayoutInfoSaver) { val titleLayoutInfo = rememberSaveable(stateSaver = TitleLayoutInfoSaver) {
@ -183,6 +191,17 @@ private fun AdventureDetailContent(
text = title.value ?: "", text = title.value ?: "",
) )
}, },
actions = {
IconButton(
onClick = onRefresh,
enabled = isLoading.value.not(),
) {
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = null,
)
}
}
) )
LazyColumn( LazyColumn(
state = lazyListState, state = lazyListState,
@ -251,6 +270,9 @@ private fun AdventureDetailPreview() {
LexiconTheme { LexiconTheme {
Surface { Surface {
AdventureDetailContent( AdventureDetailContent(
isLoading = remember {
mutableStateOf(false)
},
title = remember { title = remember {
mutableStateOf("La traque") mutableStateOf("La traque")
}, },
@ -321,6 +343,7 @@ private fun AdventureDetailPreview() {
) )
}, },
onBack = { }, onBack = { },
onRefresh = { },
) )
} }
} }

View file

@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel 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.navigation.screens.adventureDetailArgument
import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureLineUio.Style import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureLineUio.Style
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class AdventureDetailViewModel @Inject constructor( class AdventureDetailViewModel @Inject constructor(
adventureRepository: AdventureRepository, private val adventureRepository: AdventureRepository,
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
) : ViewModel() { ) : ViewModel() {
private val argument = savedStateHandle.adventureDetailArgument private val argument = savedStateHandle.adventureDetailArgument
private val detail = adventureRepository.adventureFlow( private val detail = adventureRepository.adventureFlow(
bookTitle = argument.bookTitle, documentId = argument.documentId,
adventureTitle = argument.adventureTitle, adventureTitle = argument.adventureTitle,
).stateIn( ).stateIn(
scope = viewModelScope, scope = viewModelScope,
@ -35,6 +39,9 @@ class AdventureDetailViewModel @Inject constructor(
initialValue = null, initialValue = null,
) )
private val _isLoading = mutableStateOf(false)
val isLoading: State<Boolean> get() = _isLoading
val background: State<Uri?> val background: State<Uri?>
@Composable @Composable
get() = remember(detail) { 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
}
}
}
}
} }

View file

@ -94,13 +94,15 @@ fun rememberPaddingValues(
SUB_TITLE -> 0.dp SUB_TITLE -> 0.dp
CHAPTER -> when (previous?.style) { CHAPTER -> when (previous?.style) {
TITLE -> 64.dp TITLE -> 48.dp
SUB_TITLE -> 48.dp
else -> 32.dp else -> 32.dp
} }
PARAGRAPH -> when (previous?.style) { PARAGRAPH -> when (previous?.style) {
PARAGRAPH -> 8.dp PARAGRAPH -> 8.dp
TITLE -> 64.dp TITLE -> 48.dp
SUB_TITLE -> 48.dp
else -> 16.dp else -> 16.dp
} }