Add a refresh feature for the adventure detail (was already flow <3)
This commit is contained in:
parent
5a1cedf4ff
commit
03671792d6
10 changed files with 148 additions and 54 deletions
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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é",
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue