Add specific adventure book refresh feature

This commit is contained in:
Andres Gomez, Thomas (ITDV RL) 2024-06-19 17:58:20 +02:00
parent 532f5810d7
commit 96269cf84a
13 changed files with 250 additions and 93 deletions

View file

@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 2, "version": 2,
"identityHash": "ad9094e2a7611443722a5415154015bf", "identityHash": "eecd0da0c8ae6578a5d36c3b926c2fe8",
"entities": [ "entities": [
{ {
"tableName": "lexicon", "tableName": "lexicon",
@ -321,7 +321,7 @@
}, },
{ {
"tableName": "adventures", "tableName": "adventures",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookTitle` TEXT NOT NULL, `adventureTitle` TEXT NOT NULL, `adventureCategory` TEXT, `bookIcon` TEXT, `adventureBackground` TEXT, `index` INTEGER NOT NULL, `revision` INTEGER NOT NULL, PRIMARY KEY(`bookTitle`, `adventureTitle`))", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookTitle` TEXT NOT NULL, `adventureTitle` TEXT NOT NULL, `adventureCategory` TEXT, `bookIcon` TEXT, `adventureBackground` TEXT, `index` INTEGER NOT NULL, `revision` INTEGER NOT NULL, `documentId` TEXT NOT NULL, PRIMARY KEY(`bookTitle`, `adventureTitle`))",
"fields": [ "fields": [
{ {
"fieldPath": "bookTitle", "fieldPath": "bookTitle",
@ -364,6 +364,12 @@
"columnName": "revision", "columnName": "revision",
"affinity": "INTEGER", "affinity": "INTEGER",
"notNull": true "notNull": true
},
{
"fieldPath": "documentId",
"columnName": "documentId",
"affinity": "TEXT",
"notNull": true
} }
], ],
"primaryKey": { "primaryKey": {
@ -440,7 +446,7 @@
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ad9094e2a7611443722a5415154015bf')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'eecd0da0c8ae6578a5d36c3b926c2fe8')"
] ]
} }
} }

View file

@ -15,6 +15,7 @@ data class AdventureDbo(
val adventureBackground: String?, val adventureBackground: String?,
val index: Int, val index: Int,
val revision: Long, val revision: Long,
val documentId: String,
) )
@Entity( @Entity(

View file

@ -8,5 +8,6 @@ data class Adventure(
val adventureCategory: String?, val adventureCategory: String?,
val adventureTitle: String, val adventureTitle: String,
val adventureBackground: Uri?, val adventureBackground: Uri?,
val documentId: String,
val story: List<AdventureLine>, val story: List<AdventureLine>,
) )

View file

@ -4,6 +4,6 @@ import android.net.Uri
data class AdventureBook( data class AdventureBook(
val title: String, val title: String,
val document: String, val documentId: String,
val icon: Uri?, val icon: Uri?,
) )

View file

@ -23,7 +23,7 @@ class AdventureBookParser @Inject constructor() {
if (character != null && document != null) { if (character != null && document != null) {
val adventure = AdventureBook( val adventure = AdventureBook(
title = character, title = character,
document = document, documentId = document,
icon = row.parseUri(ICON), icon = row.parseUri(ICON),
) )
adventures.add(adventure) adventures.add(adventure)

View file

@ -24,6 +24,7 @@ class AdventureDboFactory @Inject constructor() {
adventureTitle = adventure.adventureTitle, adventureTitle = adventure.adventureTitle,
adventureCategory = adventure.adventureCategory, adventureCategory = adventure.adventureCategory,
adventureBackground = adventure.adventureBackground.toUriOrNull(), adventureBackground = adventure.adventureBackground.toUriOrNull(),
documentId = adventure.documentId,
story = stories story = stories
.filter { .filter {
it.bookTitle == adventure.bookTitle && it.adventureTitle == adventure.adventureTitle it.bookTitle == adventure.bookTitle && it.adventureTitle == adventure.adventureTitle
@ -57,6 +58,7 @@ class AdventureDboFactory @Inject constructor() {
adventureBackground = story.background.toString(), adventureBackground = story.background.toString(),
index = index, index = index,
revision = story.revision, revision = story.revision,
documentId = book.documentId,
) )
fun convertToStoryDbo( fun convertToStoryDbo(

View file

@ -2,6 +2,8 @@ package com.pixelized.rplexicon.data.repository.adventure
import com.pixelized.rplexicon.data.database.CompanionDatabase import com.pixelized.rplexicon.data.database.CompanionDatabase
import com.pixelized.rplexicon.data.database.adventure.AdventureDao import com.pixelized.rplexicon.data.database.adventure.AdventureDao
import com.pixelized.rplexicon.data.database.adventure.AdventureDbo
import com.pixelized.rplexicon.data.database.adventure.AdventureStoryDbo
import com.pixelized.rplexicon.data.model.adventure.Adventure import com.pixelized.rplexicon.data.model.adventure.Adventure
import com.pixelized.rplexicon.data.model.adventure.AdventureBook import com.pixelized.rplexicon.data.model.adventure.AdventureBook
import com.pixelized.rplexicon.data.model.adventure.AdventureLine import com.pixelized.rplexicon.data.model.adventure.AdventureLine
@ -45,47 +47,23 @@ class AdventureRepository @Inject constructor(
val database = database.adventureDao() val database = database.adventureDao()
val bookStore = database.findAdventures() val bookStore = database.findAdventures()
val bookToRemove = bookStore.associate { val bookToRemove: MutableMap<String, Boolean> = bookStore.associate {
it.bookTitle to true it.bookTitle to true
}.toMutableMap() }.toMutableMap()
val storyToRemove = bookStore.associate {
val storyToRemove: MutableMap<Pair<String, String>, Boolean> = bookStore.associate {
(it.bookTitle to it.adventureTitle) to true (it.bookTitle to it.adventureTitle) to true
}.toMutableMap() }.toMutableMap()
val adventures = fetchAdventureBook().flatMap { book -> val adventures = fetchAdventureBooks().flatMap { book ->
// tag this book to keep it in the database // tag this book to keep it in the database
bookToRemove[book.title] = false bookToRemove[book.title] = false
val stories = fetchAdventureStory(book = book) fetchAdventuresContent(
stories.mapNotNull { story -> database = database,
// tag this story to keep it in the database storyToRemove = storyToRemove,
storyToRemove[book.title to story.title] = false book = book,
)
val adventure = adventureDboFactory.convertToAdventureDbo(
book = book,
story = story,
index = stories.indexOf(story),
)
val cache = database.findAdventure(
bookTitle = book.title,
adventureTitle = story.title,
)
if (cache == null || cache.revision < adventure.revision) {
val lines = fetchAdventureLine(book = book, story = story)
.mapIndexed { index, line ->
adventureDboFactory.convertToStoryDbo(
book = book,
story = story,
index = index,
line = line
)
}
adventure to lines
} else {
null
}
}
} }
database.update( database.update(
@ -99,6 +77,78 @@ class AdventureRepository @Inject constructor(
) )
} }
@Throws(IncompatibleSheetStructure::class, Exception::class)
suspend fun fetchAdventures(
bookTitle: String,
documentId: String,
) {
val database = database.adventureDao()
val bookStore = database.findAdventures(bookTitle = bookTitle)
val storyToRemove: MutableMap<Pair<String, String>, Boolean> = bookStore.associate {
(it.bookTitle to it.adventureTitle) to true
}.toMutableMap()
val adventures = fetchAdventuresContent(
database = database,
storyToRemove = storyToRemove,
book = AdventureBook(
title = bookTitle,
documentId = documentId,
icon = null,
),
)
database.update(
booksToRemove = emptyList(),
storiesToRemove = storyToRemove
.filter { it.value }
.map { AdventureDao.StoryPartialId(it.key.first, it.key.second) },
adventure = adventures,
)
}
private suspend fun fetchAdventuresContent(
database: AdventureDao,
storyToRemove: MutableMap<Pair<String, String>, Boolean>,
book: AdventureBook
): List<Pair<AdventureDbo, List<AdventureStoryDbo>>> {
val stories = fetchAdventureStory(
documentId = book.documentId,
)
return stories.mapNotNull { story ->
// tag this story to keep it in the database
storyToRemove[book.title to story.title] = false
val adventure = adventureDboFactory.convertToAdventureDbo(
book = book,
story = story,
index = stories.indexOf(story),
)
val cache = database.findAdventure(
bookTitle = book.title,
adventureTitle = story.title,
)
if (cache == null || cache.revision < adventure.revision) {
val lines = fetchAdventureLine(
documentId = book.documentId,
adventureTitle = story.title
).mapIndexed { index, line ->
adventureDboFactory.convertToStoryDbo(
book = book,
story = story,
index = index,
line = line
)
}
adventure to lines
} else {
null
}
}
}
fun find(bookTitle: String): List<Adventure> { fun find(bookTitle: String): List<Adventure> {
return adventure.value.filter { adventure -> return adventure.value.filter { adventure ->
adventure.bookTitle == bookTitle adventure.bookTitle == bookTitle
@ -112,7 +162,7 @@ class AdventureRepository @Inject constructor(
} }
@Throws(IncompatibleSheetStructure::class, Exception::class) @Throws(IncompatibleSheetStructure::class, Exception::class)
private suspend fun fetchAdventureBook(): List<AdventureBook> { private suspend fun fetchAdventureBooks(): List<AdventureBook> {
return googleRepository.fetch { sheet -> return googleRepository.fetch { sheet ->
val request = sheet.get(Adventures.ID, Adventures.ADVENTURES) val request = sheet.get(Adventures.ID, Adventures.ADVENTURES)
adventureBookParser.parse(sheet = request.execute()) adventureBookParser.parse(sheet = request.execute())
@ -120,20 +170,20 @@ class AdventureRepository @Inject constructor(
} }
@Throws(IncompatibleSheetStructure::class, Exception::class) @Throws(IncompatibleSheetStructure::class, Exception::class)
private suspend fun fetchAdventureStory(book: AdventureBook): List<AdventureStory> { private suspend fun fetchAdventureStory(documentId: String): List<AdventureStory> {
return googleRepository.fetch { sheet -> return googleRepository.fetch { sheet ->
val request = sheet.get(book.document, Adventures.ADVENTURES) val request = sheet.get(documentId, Adventures.ADVENTURES)
adventureStoryParser.parse(sheet = request.execute()) adventureStoryParser.parse(sheet = request.execute())
} }
} }
private suspend fun fetchAdventureLine( private suspend fun fetchAdventureLine(
book: AdventureBook, documentId: String,
story: AdventureStory adventureTitle: String,
): List<AdventureLine> { ): List<AdventureLine> {
return try { return try {
googleRepository.fetch { sheet -> googleRepository.fetch { sheet ->
val request = sheet.get(book.document, story.title) val request = sheet.get(documentId, adventureTitle)
adventureStoryLineParser.parse(sheet = request.execute()) adventureStoryLineParser.parse(sheet = request.execute())
} }
} catch (exception: Exception) { } catch (exception: Exception) {

View file

@ -15,19 +15,23 @@ 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 BOOK_TITLE_ARG = "bookTitle"
private const val DOCUMENT_ID_ARG = "documentId"
val ADVENTURE_CHAPTER_ROUTE = "$ROUTE?${BOOK_TITLE_ARG.ARG}" val ADVENTURE_CHAPTER_ROUTE = "$ROUTE?${BOOK_TITLE_ARG.ARG}&${DOCUMENT_ID_ARG.ARG}"
@Stable @Stable
@Immutable @Immutable
class AdventureChapterArgument( class AdventureChapterArgument(
val bookTitle: String, val bookTitle: String,
val documentId: String,
) )
val SavedStateHandle.adventureChaptersArgument: AdventureChapterArgument val SavedStateHandle.adventureChaptersArgument: AdventureChapterArgument
get() = AdventureChapterArgument( get() = AdventureChapterArgument(
bookTitle = get(BOOK_TITLE_ARG) bookTitle = get(BOOK_TITLE_ARG)
?: error("AdventureDetailArgument missing argument: $BOOK_TITLE_ARG"), ?: error("AdventureDetailArgument missing argument: $BOOK_TITLE_ARG"),
documentId = get(DOCUMENT_ID_ARG)
?: error("AdventureDetailArgument missing argument: $BOOK_TITLE_ARG"),
) )
fun NavGraphBuilder.composableAdventureChapters() { fun NavGraphBuilder.composableAdventureChapters() {
@ -39,6 +43,10 @@ fun NavGraphBuilder.composableAdventureChapters() {
type = NavType.StringType type = NavType.StringType
nullable = false nullable = false
}, },
navArgument(name = DOCUMENT_ID_ARG) {
type = NavType.StringType
nullable = false
},
) )
) { ) {
AdventureChaptersScreen() AdventureChaptersScreen()
@ -47,8 +55,9 @@ fun NavGraphBuilder.composableAdventureChapters() {
fun NavHostController.navigateToAdventureChapters( fun NavHostController.navigateToAdventureChapters(
bookTitle: String, bookTitle: String,
documentId: String,
option: NavOptionsBuilder.() -> Unit = {}, option: NavOptionsBuilder.() -> Unit = {},
) { ) {
val route = "$ROUTE?$BOOK_TITLE_ARG=$bookTitle" val route = "$ROUTE?$BOOK_TITLE_ARG=$bookTitle&$DOCUMENT_ID_ARG=$documentId"
navigate(route = route, builder = option) navigate(route = route, builder = option)
} }

View file

@ -31,6 +31,7 @@ import com.pixelized.rplexicon.utilitary.extentions.lexicon
data class AdventureBookUio( data class AdventureBookUio(
val bookTitle: String, val bookTitle: String,
val bookIcon: Uri?, val bookIcon: Uri?,
val documentId: String,
) )
@Composable @Composable
@ -80,6 +81,7 @@ private fun AdventureItemPreview() {
item = AdventureBookUio( item = AdventureBookUio(
bookIcon = null, bookIcon = null,
bookTitle = "Les chroniques d'une orc", bookTitle = "Les chroniques d'une orc",
documentId = "",
), ),
onClick = {}, onClick = {},
) )

View file

@ -67,11 +67,16 @@ fun AdventureBooksScreen(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.systemBarsPadding(), .systemBarsPadding(),
items = viewModel.books,
refreshState = refresh, refreshState = refresh,
refreshing = viewModel.isLoading, refreshing = viewModel.isLoading,
items = viewModel.books,
onBack = { screen.popBackStack() }, onBack = { screen.popBackStack() },
onBook = { screen.navigateToAdventureChapters(bookTitle = it.bookTitle) }, onBook = {
screen.navigateToAdventureChapters(
bookTitle = it.bookTitle,
documentId = it.documentId,
)
},
) )
} }
} }
@ -82,9 +87,9 @@ private fun AdventureListContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
gridState: LazyGridState = rememberLazyGridState(), gridState: LazyGridState = rememberLazyGridState(),
paddingValues: PaddingValues = PaddingValues(all = 16.dp), paddingValues: PaddingValues = PaddingValues(all = 16.dp),
items: State<List<AdventureBookUio>>,
refreshState: PullRefreshState, refreshState: PullRefreshState,
refreshing: State<Boolean>, refreshing: State<Boolean>,
items: State<List<AdventureBookUio>>,
onBack: () -> Unit, onBack: () -> Unit,
onBook: (AdventureBookUio) -> Unit, onBook: (AdventureBookUio) -> Unit,
) { ) {
@ -113,7 +118,9 @@ private fun AdventureListContent(
contentAlignment = Alignment.TopCenter, contentAlignment = Alignment.TopCenter,
) { ) {
LazyVerticalGrid( LazyVerticalGrid(
modifier = Modifier.fillMaxSize().pullRefresh(state = refreshState), modifier = Modifier
.fillMaxSize()
.pullRefresh(state = refreshState),
state = gridState, state = gridState,
columns = GridCells.Fixed(3), columns = GridCells.Fixed(3),
contentPadding = paddingValues, contentPadding = paddingValues,
@ -152,21 +159,22 @@ private fun AdventureListPreview(
) { ) {
AdventureListContent( AdventureListContent(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
refreshState = rememberPullRefreshState(
refreshing = false,
onRefresh = { },
),
refreshing = remember { mutableStateOf(false) },
items = remember { items = remember {
mutableStateOf( mutableStateOf(
listOf( listOf(
AdventureBookUio( AdventureBookUio(
bookTitle = "Les chroniques d'une orc", bookTitle = "Les chroniques d'une orc",
bookIcon = null, bookIcon = null,
documentId = "",
) )
) )
) )
}, },
refreshState = rememberPullRefreshState(
refreshing = false,
onRefresh = { },
),
refreshing = remember { mutableStateOf(false) },
onBack = { }, onBack = { },
onBook = { }, onBook = { },
) )

View file

@ -36,6 +36,7 @@ class AdventureBooksViewModel @Inject constructor(
AdventureBookUio( AdventureBookUio(
bookTitle = adventure.bookTitle, bookTitle = adventure.bookTitle,
bookIcon = adventure.bookIcon, bookIcon = adventure.bookIcon,
documentId = adventure.documentId,
) )
} }
.toSet() .toSet()

View file

@ -2,6 +2,7 @@ package com.pixelized.rplexicon.ui.screens.adventure.chapter
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -11,6 +12,10 @@ 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.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshState
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
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
@ -22,6 +27,8 @@ 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.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
@ -29,17 +36,27 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.R import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.composable.Loader
import com.pixelized.rplexicon.ui.composable.rememberAnimatedShadow 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.navigateToAdventureDetail import com.pixelized.rplexicon.ui.navigation.screens.navigateToAdventureDetail
import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.ui.theme.LexiconTheme
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun AdventureChaptersScreen( fun AdventureChaptersScreen(
viewModel: AdventureChaptersViewModel = hiltViewModel(), viewModel: AdventureChaptersViewModel = hiltViewModel(),
) { ) {
val screen = LocalScreenNavHost.current val screen = LocalScreenNavHost.current
val scope = rememberCoroutineScope()
val refresh = rememberPullRefreshState(
refreshing = false,
onRefresh = {
scope.launch { viewModel.update() }
},
)
Surface( Surface(
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
@ -48,6 +65,8 @@ fun AdventureChaptersScreen(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.systemBarsPadding(), .systemBarsPadding(),
refreshState = refresh,
refreshing = viewModel.isLoading,
bookTitle = viewModel.bookTitle, bookTitle = viewModel.bookTitle,
chapters = viewModel.chapters, chapters = viewModel.chapters,
onChapter = { onChapter = {
@ -61,12 +80,14 @@ fun AdventureChaptersScreen(
} }
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable @Composable
private fun AdventureChapterContent( private fun AdventureChapterContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
lazyListState: LazyListState = rememberLazyListState(), lazyListState: LazyListState = rememberLazyListState(),
paddingValues: PaddingValues = PaddingValues(vertical = 16.dp), paddingValues: PaddingValues = PaddingValues(vertical = 16.dp),
refreshState: PullRefreshState,
refreshing: State<Boolean>,
bookTitle: State<String>, bookTitle: State<String>,
chapters: State<List<AdventureChapterUio>>, chapters: State<List<AdventureChapterUio>>,
onChapter: (AdventureChapterUio.AdventureItem) -> Unit, onChapter: (AdventureChapterUio.AdventureItem) -> Unit,
@ -91,38 +112,48 @@ private fun AdventureChapterContent(
}, },
) )
}, },
content = { it -> content = {
LazyColumn( Box(
modifier = Modifier.padding(paddingValues = it), modifier = Modifier.padding(paddingValues = it),
state = lazyListState, contentAlignment = Alignment.TopCenter,
contentPadding = paddingValues,
verticalArrangement = Arrangement.spacedBy(space = 8.dp),
) { ) {
itemsIndexed(items = chapters.value) { index, item -> LazyColumn(
when (item) { modifier = Modifier.pullRefresh(state = refreshState),
is AdventureChapterUio.AdventureCategory -> { state = lazyListState,
AdventureChapterCategory( contentPadding = paddingValues,
modifier = Modifier verticalArrangement = Arrangement.spacedBy(space = 8.dp),
.padding(top = if (index != 0) 32.dp else 0.dp) ) {
.fillMaxWidth(), itemsIndexed(items = chapters.value) { index, item ->
category = item, when (item) {
) is AdventureChapterUio.AdventureCategory -> {
} AdventureChapterCategory(
modifier = Modifier
.padding(top = if (index != 0) 32.dp else 0.dp)
.fillMaxWidth(),
category = item,
)
}
is AdventureChapterUio.AdventureItem -> { is AdventureChapterUio.AdventureItem -> {
AdventureChapterItem( AdventureChapterItem(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
item = item, item = item,
onClick = { onChapter(item) }, onClick = { onChapter(item) },
) )
}
} }
} }
} }
Loader(
refreshState = refreshState,
refreshing = refreshing,
)
} }
} }
) )
} }
@OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@ -131,6 +162,11 @@ private fun AdventureChapterPreview() {
Surface { Surface {
AdventureChapterContent( AdventureChapterContent(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
refreshState = rememberPullRefreshState(
refreshing = false,
onRefresh = { },
),
refreshing = remember { mutableStateOf(false) },
bookTitle = remember { bookTitle = remember {
mutableStateOf( mutableStateOf(
"Les chroniques d'une orc" "Les chroniques d'une orc"

View file

@ -1,41 +1,82 @@
package com.pixelized.rplexicon.ui.screens.adventure.chapter package com.pixelized.rplexicon.ui.screens.adventure.chapter
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf 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 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.Structure
import com.pixelized.rplexicon.ui.composable.error.FetchErrorUio.Structure.Type.ADVENTURE
import com.pixelized.rplexicon.ui.navigation.screens.adventureChaptersArgument import com.pixelized.rplexicon.ui.navigation.screens.adventureChaptersArgument
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class AdventureChaptersViewModel @Inject constructor( class AdventureChaptersViewModel @Inject constructor(
adventureRepository: AdventureRepository, private val adventureRepository: AdventureRepository,
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
) : ViewModel() { ) : ViewModel() {
private val argument = savedStateHandle.adventureChaptersArgument private val argument = savedStateHandle.adventureChaptersArgument
private val _chapters = mutableStateOf(
adventureRepository.find(bookTitle = argument.bookTitle) private val _error = MutableSharedFlow<FetchErrorUio>()
)
private val _isLoading = mutableStateOf(false)
val isLoading: State<Boolean> get() = _isLoading
val bookTitle = derivedStateOf { val bookTitle = derivedStateOf {
argument.bookTitle argument.bookTitle
} }
val chapters = derivedStateOf {
_chapters.value private val _chapters = adventureRepository.adventure
.groupBy { it.adventureCategory } .map { adventures ->
.flatMap { entry -> adventures
val header = entry.key?.let { .filter { it.bookTitle == argument.bookTitle && it.documentId == argument.documentId }
listOf(AdventureChapterUio.AdventureCategory(title = it)) .groupBy { it.adventureCategory ?: "" }
} ?: emptyList() .flatMap { entry ->
val stories = entry.value.map { val header = listOf(
AdventureChapterUio.AdventureItem( AdventureChapterUio.AdventureCategory(title = entry.key)
bookTitle = it.bookTitle,
adventureTitle = it.adventureTitle,
) )
val stories = entry.value.map {
AdventureChapterUio.AdventureItem(
bookTitle = it.bookTitle,
adventureTitle = it.adventureTitle,
)
}
header + stories
} }
header + stories }
val chapters: State<List<AdventureChapterUio>>
@Composable
@Stable
get() = _chapters.collectAsState(initial = emptyList())
suspend fun update() {
try {
withContext(Dispatchers.Main) {
_isLoading.value = true
} }
withContext(Dispatchers.Default) {
adventureRepository.fetchAdventures(
bookTitle = argument.bookTitle,
documentId = argument.documentId,
)
}
} catch (_: Exception) {
_error.emit(value = Structure(ADVENTURE))
} finally {
withContext(Dispatchers.Main) {
_isLoading.value = false
}
}
} }
} }