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(
bookTitle: String,
documentId: String,
adventureTitle: String,
): Flow<Adventure?> {
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<AdventureBook> {
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
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)
}

View file

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

View file

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

View file

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

View file

@ -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é",
),

View file

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

View file

@ -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<Boolean>,
title: State<String?>,
titleIndex: State<Int>,
background: State<Uri?>,
adventures: State<List<AdventureLineUio>>,
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 = { },
)
}
}

View file

@ -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<Boolean> get() = _isLoading
val background: State<Uri?>
@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
}
}
}
}
}

View file

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