Refactor the Adventure Book/Story/Line model. Avoid clearing of already downloaded stories.

This commit is contained in:
Andres Gomez, Thomas (ITDV RL) 2024-07-23 13:41:28 +02:00
parent 8cfa0ac272
commit 250d9cbb7c
13 changed files with 342 additions and 229 deletions

View file

@ -36,56 +36,67 @@ interface AdventureDao {
@Query("SELECT * from ${AdventureLineDbo.TABLE} where documentId = :documentId AND title = :storyTitle") @Query("SELECT * from ${AdventureLineDbo.TABLE} where documentId = :documentId AND title = :storyTitle")
fun fetchAdventureLine(documentId: String, storyTitle: String): List<AdventureLineDbo> fun fetchAdventureLine(documentId: String, storyTitle: String): List<AdventureLineDbo>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertBook(book: AdventureBookDbo) fun insertBook(book: AdventureBookDbo): Long
@Update @Update
fun updateBook(book: AdventureBookDbo) fun updateBook(book: AdventureBookDbo)
fun insertOrUpdateBook(book: AdventureBookDbo) {
if (insertBook(book = book) == 1L) {
updateBook(book = book)
}
}
@Delete(entity = AdventureBookDbo::class) @Delete(entity = AdventureBookDbo::class)
fun deleteBook(id: AdventureBookDbo.BookId) fun deleteBook(id: AdventureBookDbo.BookId)
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertStory(story: AdventureStoryDbo) fun insertStory(story: AdventureStoryDbo): Long
@Update @Update
fun updateStory(story: AdventureStoryDbo) fun updateStory(story: AdventureStoryDbo)
fun insertOrUpdateStory(story: AdventureStoryDbo) {
if (insertStory(story = story) == 1L) {
updateStory(story = story)
}
}
@Delete(entity = AdventureStoryDbo::class) @Delete(entity = AdventureStoryDbo::class)
fun deleteStory(id: AdventureStoryDbo.StoryId) fun deleteStory(id: AdventureStoryDbo.StoryId)
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertLine(line: AdventureLineDbo) fun insertLine(line: AdventureLineDbo): Long
@Update @Update
fun updateLine(line: AdventureLineDbo) fun updateLine(line: AdventureLineDbo)
fun insertOrUpdateLine(line: AdventureLineDbo) {
if (insertLine(line = line) == 1L) {
updateLine(line = line)
}
}
@Delete(entity = AdventureLineDbo::class) @Delete(entity = AdventureLineDbo::class)
fun deleteLines(id: AdventureLineDbo.LineId) fun deleteLines(id: AdventureLineDbo.LineId)
@Transaction @Transaction
fun update( fun update(
booksToInsert: List<AdventureBookDbo>, booksToInsertOrUpdate: List<AdventureBookDbo> = emptyList(),
booksToUpdate: List<AdventureBookDbo>, booksToRemove: List<AdventureBookDbo.BookId> = emptyList(),
booksToRemove: List<AdventureBookDbo.BookId>, storiesToInsertOrUpdate: List<AdventureStoryDbo> = emptyList(),
storiesToInsert: List<AdventureStoryDbo>, storiesToRemove: List<AdventureStoryDbo.StoryId> = emptyList(),
storiesToUpdate: List<AdventureStoryDbo>, linesToInsertOrUpdate: List<AdventureLineDbo> = emptyList(),
storiesToRemove: List<AdventureStoryDbo.StoryId>, linesToRemove: List<AdventureLineDbo.LineId> = emptyList(),
linesToInsert: List<AdventureLineDbo>,
linesToUpdate: List<AdventureLineDbo>,
linesToRemove: List<AdventureLineDbo.LineId>,
) { ) {
// First remove the stuff // First remove the stuff
linesToRemove.forEach { deleteLines(id = it) } linesToRemove.forEach { deleteLines(id = it) }
storiesToRemove.forEach { deleteStory(id = it) } storiesToRemove.forEach { deleteStory(id = it) }
booksToRemove.forEach { deleteBook(id = it) } booksToRemove.forEach { deleteBook(id = it) }
// then insert the stuff // then insert or update the stuff
booksToInsert.forEach { insertBook(book = it) } booksToInsertOrUpdate.forEach { insertOrUpdateBook(book = it) }
storiesToInsert.forEach { insertStory(story = it) } storiesToInsertOrUpdate.forEach { insertOrUpdateStory(story = it) }
linesToInsert.forEach { insertLine(line = it) } linesToInsertOrUpdate.forEach { insertOrUpdateLine(line = it) }
// and update the stuff
booksToUpdate.forEach { updateBook(book = it) }
storiesToUpdate.forEach { updateStory(story = it) }
linesToUpdate.forEach { updateLine(line = it) }
} }
} }

View file

@ -1,13 +0,0 @@
package com.pixelized.rplexicon.data.model.adventure
import android.net.Uri
data class Adventure(
val bookTitle: String,
val bookIcon: Uri?,
val storyCategory: String?,
val storyTitle: String,
val storyBackground: Uri?,
val documentId: String,
val story: List<AdventureLine>,
)

View file

@ -1,16 +1,6 @@
package com.pixelized.rplexicon.data.model.adventure package com.pixelized.rplexicon.data.model.adventure
data class AdventureLine( data class AdventureLine(
val format: Format, val format: AdventureLineFormat,
val text: String, val text: String,
) { )
enum class Format {
TITLE,
SUB_TITLE,
CHAPTER,
PARAGRAPH,
DIALOGUE,
ANNEX,
LEGEND,
}
}

View file

@ -0,0 +1,11 @@
package com.pixelized.rplexicon.data.model.adventure
enum class AdventureLineFormat {
TITLE,
SUB_TITLE,
CHAPTER,
PARAGRAPH,
DIALOGUE,
ANNEX,
LEGEND,
}

View file

@ -0,0 +1,23 @@
package com.pixelized.rplexicon.data.model.adventure
import android.net.Uri
data class Book(
val documentId: String,
val title: String,
val icon: Uri?,
val updating: Boolean,
val stories: List<Story>,
) {
data class Story(
val title: String,
val category: String?,
val background: Uri?,
val lines: List<Line>,
) {
data class Line(
val format: AdventureLineFormat,
val text: String,
)
}
}

View file

@ -2,6 +2,7 @@ package com.pixelized.rplexicon.data.parser.adventure
import com.google.api.services.sheets.v4.model.ValueRange import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.data.model.adventure.AdventureLine import com.pixelized.rplexicon.data.model.adventure.AdventureLine
import com.pixelized.rplexicon.data.model.adventure.AdventureLineFormat
import com.pixelized.rplexicon.utilitary.extentions.sheet import com.pixelized.rplexicon.utilitary.extentions.sheet
import javax.inject.Inject import javax.inject.Inject
@ -22,15 +23,15 @@ class AdventureStoryLineParser @Inject constructor() {
} }
} }
private fun ArrayList<*>.parseFormat(): AdventureLine.Format? = private fun ArrayList<*>.parseFormat(): AdventureLineFormat? =
when (this.getOrNull(0) as? String) { when (this.getOrNull(0) as? String) {
TITLE -> AdventureLine.Format.TITLE TITLE -> AdventureLineFormat.TITLE
SUB_TITLE -> AdventureLine.Format.SUB_TITLE SUB_TITLE -> AdventureLineFormat.SUB_TITLE
CHAPTER -> AdventureLine.Format.CHAPTER CHAPTER -> AdventureLineFormat.CHAPTER
PARAGRAPH -> AdventureLine.Format.PARAGRAPH PARAGRAPH -> AdventureLineFormat.PARAGRAPH
DIALOGUE -> AdventureLine.Format.DIALOGUE DIALOGUE -> AdventureLineFormat.DIALOGUE
ANNEX -> AdventureLine.Format.ANNEX ANNEX -> AdventureLineFormat.ANNEX
LEGEND -> AdventureLine.Format.LEGEND LEGEND -> AdventureLineFormat.LEGEND
else -> null else -> null
} }

View file

@ -3,47 +3,54 @@ package com.pixelized.rplexicon.data.repository.adventure
import com.pixelized.rplexicon.data.database.adventure.AdventureBookDbo import com.pixelized.rplexicon.data.database.adventure.AdventureBookDbo
import com.pixelized.rplexicon.data.database.adventure.AdventureLineDbo import com.pixelized.rplexicon.data.database.adventure.AdventureLineDbo
import com.pixelized.rplexicon.data.database.adventure.AdventureStoryDbo import com.pixelized.rplexicon.data.database.adventure.AdventureStoryDbo
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
import com.pixelized.rplexicon.data.model.adventure.AdventureLineFormat
import com.pixelized.rplexicon.data.model.adventure.AdventureStory import com.pixelized.rplexicon.data.model.adventure.AdventureStory
import com.pixelized.rplexicon.data.model.adventure.Book
import com.pixelized.rplexicon.utilitary.extentions.string.toUriOrNull import com.pixelized.rplexicon.utilitary.extentions.string.toUriOrNull
import javax.inject.Inject import javax.inject.Inject
class AdventureDboFactory @Inject constructor() { class AdventureDboFactory @Inject constructor() {
fun convertFromDbo( fun convertFromDbo(
updatingBookId: String?,
books: List<AdventureBookDbo>, books: List<AdventureBookDbo>,
stories: List<AdventureStoryDbo>, stories: List<AdventureStoryDbo>,
lines: List<AdventureLineDbo>, lines: List<AdventureLineDbo>,
): List<Adventure> { ): List<Book> {
return books.flatMap { book: AdventureBookDbo -> val indexedStories = stories.groupBy { it.documentId }
stories val indexedLines = lines.groupBy { it.documentId to it.story }
.filter { story -> story.documentId == book.documentId } return books.map { book: AdventureBookDbo ->
.sortedBy { story -> story.index } Book(
.map { story: AdventureStoryDbo -> documentId = book.documentId,
Adventure( title = book.title,
bookTitle = book.title, icon = book.icon.toUriOrNull(),
bookIcon = book.icon.toUriOrNull(), updating = book.documentId == updatingBookId,
documentId = book.documentId, stories = indexedStories[book.documentId]
storyTitle = story.title, ?.sortedBy { story -> story.index }
storyCategory = story.category, ?.map { story ->
storyBackground = story.background.toUriOrNull(), Book.Story(
story = lines title = story.title,
.filter { it.documentId == story.documentId && it.story == story.title } category = story.category,
.sortedBy { it.index } background = story.background.toUriOrNull(),
.map { line -> lines = indexedLines[book.documentId to story.title]
AdventureLine( ?.sortedBy { it.index }
text = line.text, ?.map { line ->
format = try { Book.Story.Line(
AdventureLine.Format.valueOf(line.format) text = line.text,
} catch (_: Exception) { format = try {
AdventureLine.Format.PARAGRAPH AdventureLineFormat.valueOf(line.format)
}, } catch (_: Exception) {
) AdventureLineFormat.PARAGRAPH
}, },
) )
} }
?: emptyList(),
)
}
?: emptyList(),
)
} }
} }

View file

@ -4,7 +4,7 @@ import com.pixelized.rplexicon.data.database.CompanionDatabase
import com.pixelized.rplexicon.data.database.adventure.AdventureBookDbo import com.pixelized.rplexicon.data.database.adventure.AdventureBookDbo
import com.pixelized.rplexicon.data.database.adventure.AdventureLineDbo import com.pixelized.rplexicon.data.database.adventure.AdventureLineDbo
import com.pixelized.rplexicon.data.database.adventure.AdventureStoryDbo import com.pixelized.rplexicon.data.database.adventure.AdventureStoryDbo
import com.pixelized.rplexicon.data.model.adventure.Adventure import com.pixelized.rplexicon.data.model.adventure.Book
import com.pixelized.rplexicon.data.parser.adventure.AdventureBookParser import com.pixelized.rplexicon.data.parser.adventure.AdventureBookParser
import com.pixelized.rplexicon.data.parser.adventure.AdventureStoryLineParser import com.pixelized.rplexicon.data.parser.adventure.AdventureStoryLineParser
import com.pixelized.rplexicon.data.parser.adventure.AdventureStoryParser import com.pixelized.rplexicon.data.parser.adventure.AdventureStoryParser
@ -17,6 +17,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -24,7 +25,6 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
import kotlin.math.min
@Singleton @Singleton
class AdventureRepository @Inject constructor( class AdventureRepository @Inject constructor(
@ -41,13 +41,16 @@ class AdventureRepository @Inject constructor(
var lastSuccessFullUpdate: Update = Update.INITIAL var lastSuccessFullUpdate: Update = Update.INITIAL
private set private set
private val updatingBookId = MutableStateFlow<String?>(null)
private val adventures = database.let { database -> private val adventures = database.let { database ->
combine( combine(
updatingBookId,
database.getBooksFlow(), database.getBooksFlow(),
database.getStoriesFlow(), database.getStoriesFlow(),
database.getStoryLinesFlow(), database.getStoryLinesFlow(),
) { books, stories, lines -> ) { updatingBookId, books, stories, lines ->
adventureDboFactory.convertFromDbo( adventureDboFactory.convertFromDbo(
updatingBookId = updatingBookId,
books = books, books = books,
stories = stories, stories = stories,
lines = lines, lines = lines,
@ -59,49 +62,112 @@ class AdventureRepository @Inject constructor(
) )
} }
fun bookFlow(): Flow<List<Adventure>> { fun booksFlow(): Flow<List<Book>> {
return adventures return adventures
} }
fun storyFlow(bookTitle: String): Flow<List<Adventure>> { fun bookFlow(documentId: String): Flow<Book?> {
return adventures.map { adventures -> return adventures.map { adventures ->
adventures.filter { adventure -> adventures.firstOrNull { adventure ->
adventure.bookTitle == bookTitle adventure.documentId == documentId
} }
} }
} }
fun adventureFlow(documentId: String, adventureTitle: String): Flow<Adventure?> { fun storyFlow(documentId: String, storyTitle: String): Flow<Book.Story?> {
return adventures.map { adventures -> return adventures.map { adventures ->
adventures.firstOrNull { adventure -> adventures.firstNotNullOfOrNull { adventure ->
adventure.documentId == documentId && adventure.storyTitle == adventureTitle if (adventure.documentId == documentId) {
adventure.stories.firstOrNull { it.title == storyTitle }
} else {
null
}
} }
} }
} }
@Throws(IncompatibleSheetStructure::class, Exception::class) @Throws(IncompatibleSheetStructure::class, Exception::class)
suspend fun fetchBooks() = update { suspend fun fetchBooks() {
fetchAndCompareBooks() val exceptions = mutableListOf<Exception>()
val cache = database.fetchAdventureBooks()
(booksToInsert + booksToUpdate).forEach { book -> try {
fetchAndCompareStories(documentId = book.documentId) val update = service.fetchAdventureBooks().sortedBy { it.title }
val booksToRemove = cache.map { it.id }.toMutableList()
val booksToInsertOrUpdate = mutableListOf<AdventureBookDbo>()
update.forEach { item ->
booksToRemove.remove(item.id)
booksToInsertOrUpdate.add(item)
}
database.update(
booksToRemove = booksToRemove,
)
booksToInsertOrUpdate.forEach { book ->
updatingBookId.value = book.documentId
database.update(
booksToInsertOrUpdate = listOf(book),
)
update {
try {
fetchAndCompareStories(
documentId = book.documentId
)
(storiesToInsertOrUpdate).forEach { story ->
try {
fetchAndCompareLines(
documentId = story.documentId,
storyTitle = story.title
)
} catch (exception: Exception) {
exceptions.add(exception)
}
}
} catch (exception: Exception) {
exceptions.add(exception)
}
}
}
lastSuccessFullUpdate = Update.currentTime()
} catch (exception: Exception) {
exceptions.add(exception)
} finally {
updatingBookId.value = null
} }
if (exceptions.isNotEmpty()) {
(storiesToInsert + storiesToUpdate).forEach { story -> throw exceptions.first()
fetchAndCompareLines(documentId = story.documentId, storyTitle = story.title)
} }
lastSuccessFullUpdate = Update.currentTime()
} }
@Throws(IncompatibleSheetStructure::class, Exception::class) @Throws(IncompatibleSheetStructure::class, Exception::class)
suspend fun fetchStories( suspend fun fetchStories(
documentId: String, documentId: String,
) = update { ) = update {
fetchAndCompareStories(documentId = documentId) val exceptions = mutableListOf<Exception>()
try {
updatingBookId.value = documentId
fetchAndCompareStories(documentId = documentId)
(storiesToInsert + storiesToUpdate).forEach { story -> (storiesToInsertOrUpdate).forEach { story ->
fetchAndCompareLines(documentId = story.documentId, storyTitle = story.title) try {
fetchAndCompareLines(
documentId = story.documentId,
storyTitle = story.title,
)
} catch (exception: Exception) {
exceptions.add(exception)
}
}
} catch (exception: Exception) {
exceptions.add(exception)
} finally {
updatingBookId.value = null
}
if (exceptions.isNotEmpty()) {
throw exceptions.first()
} }
} }
@ -110,36 +176,31 @@ class AdventureRepository @Inject constructor(
documentId: String, documentId: String,
storyTitle: String, storyTitle: String,
) = update { ) = update {
// specific case for adventure fetching, need to update the story too as the background is stored there. val exceptions = mutableListOf<Exception>()
service.fetchAdventureStories( try {
documentId = documentId, updatingBookId.value = documentId
).firstOrNull { story -> // specific case for adventure fetching, need to update the story too as the background is stored there.
story.documentId == documentId && story.title == storyTitle service.fetchAdventureStories(
}?.let { story -> documentId = documentId,
storiesToUpdate.add(story) ).firstOrNull { story ->
} story.documentId == documentId && story.title == storyTitle
}?.let { story ->
fetchAndCompareLines(documentId = documentId, storyTitle = storyTitle) storiesToInsertOrUpdate.add(story)
}
private suspend fun DatabaseUpdateScope.fetchAndCompareBooks() {
val cache = database.fetchAdventureBooks()
val update = service.fetchAdventureBooks()
val toRemove = cache.map { it.id }.toMutableList()
val toInsert = mutableListOf<AdventureBookDbo>()
val toUpdate = mutableListOf<AdventureBookDbo>()
update.forEach { item ->
when (toRemove.remove(item.id)) {
true -> toUpdate.add(item)
else -> toInsert.add(item)
} }
}
booksToInsert.addAll(elements = toInsert) try {
booksToUpdate.addAll(elements = toUpdate) fetchAndCompareLines(documentId = documentId, storyTitle = storyTitle)
booksToRemove.addAll(elements = toRemove) } catch (exception: Exception) {
exceptions.add(exception)
}
} catch (exception: Exception) {
exceptions.add(exception)
} finally {
updatingBookId.value = null
}
if (exceptions.isNotEmpty()) {
throw exceptions.first()
}
} }
private suspend fun DatabaseUpdateScope.fetchAndCompareStories( private suspend fun DatabaseUpdateScope.fetchAndCompareStories(
@ -151,7 +212,7 @@ class AdventureRepository @Inject constructor(
): Boolean { ): Boolean {
return cache?.let { return cache?.let {
it.revision < item.revision || cache.index != item.index it.revision < item.revision || cache.index != item.index
} ?: false } ?: true
} }
val cache = database.fetchAdventureStories( val cache = database.fetchAdventureStories(
@ -164,49 +225,39 @@ class AdventureRepository @Inject constructor(
) )
val toRemove = cache.keys.toMutableList() val toRemove = cache.keys.toMutableList()
val toInsert = mutableListOf<AdventureStoryDbo>() val toInsertOrUpdate = mutableListOf<AdventureStoryDbo>()
val toUpdate = mutableListOf<AdventureStoryDbo>()
update.forEach { item -> update.forEach { item ->
when (toRemove.remove(item.id)) { toRemove.remove(item.id)
true -> if (shouldUpdate(cache = cache[item.id], item = item)) toUpdate.add(item) if (shouldUpdate(cache = cache[item.id], item = item)) {
else -> toInsert.add(item) toInsertOrUpdate.add(item)
} }
} }
storiesToInsert.addAll(elements = toInsert)
storiesToUpdate.addAll(elements = toUpdate)
storiesToRemove.addAll(elements = toRemove) storiesToRemove.addAll(elements = toRemove)
storiesToInsertOrUpdate.addAll(elements = toInsertOrUpdate)
} }
private suspend fun DatabaseUpdateScope.fetchAndCompareLines( private suspend fun DatabaseUpdateScope.fetchAndCompareLines(
documentId: String, documentId: String,
storyTitle: String, storyTitle: String,
) { ) {
val cache = database.fetchAdventureLine( val cache: List<AdventureLineDbo> = database.fetchAdventureLine(
documentId = documentId, documentId = documentId,
storyTitle = storyTitle, storyTitle = storyTitle,
) )
val update = service.fetchAdventureLine( val update: List<AdventureLineDbo> = service.fetchAdventureLine(
documentId = documentId, documentId = documentId,
storyTitle = storyTitle, storyTitle = storyTitle,
) )
// if cache is smaller than the update we need to insert // if cache is bigger than the update we need to delete the excess lines.
if (cache.size < update.size) {
(cache.size until update.size).forEach {
linesToInsert.add(update[it])
}
}
// if cache is bigger than the update we need to delete
if (update.size < cache.size) { if (update.size < cache.size) {
(update.size until cache.size).forEach { (update.size until cache.size).forEach {
linesToRemove.add(cache[it].id) linesToRemove.add(cache[it].id)
} }
} }
// then we update the rest // then we insert / update
(0 until min(cache.size, update.size)).forEach { linesToInsertOrUpdate.addAll(update)
linesToUpdate.add(update[it])
}
} }
private suspend inline fun update( private suspend inline fun update(
@ -217,27 +268,21 @@ class AdventureRepository @Inject constructor(
lambda.invoke(update) lambda.invoke(update)
database.update( database.update(
booksToInsert = update.booksToInsert, booksToInsertOrUpdate = update.booksToInsertOrUpdate,
booksToUpdate = update.booksToUpdate,
booksToRemove = update.booksToRemove, booksToRemove = update.booksToRemove,
storiesToInsert = update.storiesToInsert, storiesToInsertOrUpdate = update.storiesToInsertOrUpdate,
storiesToUpdate = update.storiesToUpdate,
storiesToRemove = update.storiesToRemove, storiesToRemove = update.storiesToRemove,
linesToInsert = update.linesToInsert, linesToInsertOrUpdate = update.linesToInsertOrUpdate,
linesToUpdate = update.linesToUpdate,
linesToRemove = update.linesToRemove, linesToRemove = update.linesToRemove,
) )
} }
private data class DatabaseUpdateScope( private data class DatabaseUpdateScope(
val booksToInsert: MutableList<AdventureBookDbo> = mutableListOf(), val booksToInsertOrUpdate: MutableList<AdventureBookDbo> = mutableListOf(),
val booksToUpdate: MutableList<AdventureBookDbo> = mutableListOf(),
val booksToRemove: MutableList<AdventureBookDbo.BookId> = mutableListOf(), val booksToRemove: MutableList<AdventureBookDbo.BookId> = mutableListOf(),
val storiesToInsert: MutableList<AdventureStoryDbo> = mutableListOf(), val storiesToInsertOrUpdate: MutableList<AdventureStoryDbo> = mutableListOf(),
val storiesToUpdate: MutableList<AdventureStoryDbo> = mutableListOf(),
val storiesToRemove: MutableList<AdventureStoryDbo.StoryId> = mutableListOf(), val storiesToRemove: MutableList<AdventureStoryDbo.StoryId> = mutableListOf(),
val linesToInsert: MutableList<AdventureLineDbo> = mutableListOf(), val linesToInsertOrUpdate: MutableList<AdventureLineDbo> = mutableListOf(),
val linesToUpdate: MutableList<AdventureLineDbo> = mutableListOf(),
val linesToRemove: MutableList<AdventureLineDbo.LineId> = mutableListOf(), val linesToRemove: MutableList<AdventureLineDbo.LineId> = mutableListOf(),
) )
@ -270,13 +315,9 @@ class AdventureRepository @Inject constructor(
documentId: String, documentId: String,
storyTitle: String, storyTitle: String,
): List<AdventureLineDbo> { ): List<AdventureLineDbo> {
return try { return googleRepository.fetch { sheet ->
googleRepository.fetch { sheet -> val request = sheet.get(documentId, storyTitle)
val request = sheet.get(documentId, storyTitle) adventureStoryLineParser.parse(sheet = request.execute())
adventureStoryLineParser.parse(sheet = request.execute())
}
} catch (exception: Exception) {
emptyList()
}.mapIndexed { index, line -> }.mapIndexed { index, line ->
adventureDboFactory.convertToDbo( adventureDboFactory.convertToDbo(
adventureLine = line, adventureLine = line,

View file

@ -1,10 +1,12 @@
package com.pixelized.rplexicon.ui.screens.adventure.book package com.pixelized.rplexicon.ui.screens.adventure.book
import android.content.res.Configuration import android.content.res.Configuration
import android.net.Uri import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -20,10 +22,13 @@ import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.R import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.composable.images.BackgroundImage import com.pixelized.rplexicon.ui.composable.images.BackgroundImage
import com.pixelized.rplexicon.ui.composable.images.rememberBackgroundGradient import com.pixelized.rplexicon.ui.composable.images.rememberBackgroundGradient
import com.pixelized.rplexicon.ui.composable.images.rememberSaturationFilter
import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.annotateWithDropCap import com.pixelized.rplexicon.utilitary.annotateWithDropCap
import com.pixelized.rplexicon.utilitary.extentions.lexicon import com.pixelized.rplexicon.utilitary.extentions.lexicon
@ -34,6 +39,7 @@ data class AdventureBookUio(
val bookIcon: Any?, val bookIcon: Any?,
val documentId: String, val documentId: String,
val adventureTitle: String?, val adventureTitle: String?,
val updating: Boolean,
) )
@Composable @Composable
@ -47,14 +53,19 @@ fun AdventureBook(
Box( Box(
modifier = Modifier modifier = Modifier
.clip(shape = shape) .clip(shape = shape)
.clickable(onClick = onClick) .clickable(enabled = item.updating.not(), onClick = onClick)
.then(other = modifier), .then(other = modifier),
) { ) {
BackgroundImage( BackgroundImage(
modifier = Modifier modifier = Modifier
.matchParentSize() .matchParentSize()
.align(alignment = Alignment.TopCenter), .align(alignment = Alignment.TopCenter),
colorFilter = null, colorFilter = rememberSaturationFilter(
saturation = animateFloatAsState(
targetValue = if (item.updating) 0f else 1f,
label = "book item saturation animation"
).value,
),
background = rememberBackgroundGradient(0.2f, 1.0f), background = rememberBackgroundGradient(0.2f, 1.0f),
contentScale = ContentScale.FillWidth, contentScale = ContentScale.FillWidth,
alignment = Alignment.TopCenter, alignment = Alignment.TopCenter,
@ -77,18 +88,38 @@ fun AdventureBook(
@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)
private fun AdventureItemPreview() { private fun AdventureItemPreview(
@PreviewParameter(AdventureItemProvider::class) preview: AdventureBookUio,
) {
LexiconTheme { LexiconTheme {
Surface { Surface {
AdventureBook( AdventureBook(
item = AdventureBookUio( modifier = Modifier
bookIcon = R.drawable.icbg_book_generic_c_unfaded, .fillMaxWidth(0.4f)
bookTitle = "Les chroniques d'une orc", .aspectRatio(ratio = 0.8f),
documentId = "", item = preview,
adventureTitle = null,
),
onClick = {}, onClick = {},
) )
} }
} }
}
private class AdventureItemProvider : PreviewParameterProvider<AdventureBookUio> {
override val values: Sequence<AdventureBookUio> = sequenceOf(
AdventureBookUio(
bookIcon = R.drawable.icbg_book_generic_c_unfaded,
bookTitle = "Les chroniques d'une orc",
documentId = "",
updating = true,
adventureTitle = null,
),
AdventureBookUio(
bookIcon = R.drawable.icbg_book_generic_c_unfaded,
bookTitle = "Les chroniques d'une orc",
documentId = "",
updating = false,
adventureTitle = null,
),
)
} }

View file

@ -177,8 +177,23 @@ private fun AdventureListPreview(
bookTitle = "Les chroniques d'une orc", bookTitle = "Les chroniques d'une orc",
bookIcon = null, bookIcon = null,
documentId = "", documentId = "",
updating = true,
adventureTitle = null, adventureTitle = null,
) ),
AdventureBookUio(
bookTitle = "Le tome de Strahd",
bookIcon = null,
documentId = "",
updating = false,
adventureTitle = null,
),
AdventureBookUio(
bookTitle = "Carnet de voyage d'Unathana",
bookIcon = null,
documentId = "",
updating = false,
adventureTitle = null,
),
) )
) )
}, },

View file

@ -29,23 +29,19 @@ class AdventureBooksViewModel @Inject constructor(
private val _isLoading = mutableStateOf(false) private val _isLoading = mutableStateOf(false)
val isLoading: State<Boolean> get() = _isLoading val isLoading: State<Boolean> get() = _isLoading
private val _books = adventureRepository.bookFlow() private val _books = adventureRepository.booksFlow()
.map { adventures -> .map { adventures ->
adventures adventures
.groupBy { it.documentId } .sortedBy { it.title }
.mapValues { (_, adventures) -> .map { book ->
val adventure = adventures.first()
AdventureBookUio( AdventureBookUio(
bookTitle = adventure.bookTitle, bookTitle = book.title,
bookIcon = adventure.bookIcon, bookIcon = book.icon,
documentId = adventure.documentId, documentId = book.documentId,
adventureTitle = if (adventures.size == 1) adventure.storyTitle else null updating = book.updating,
adventureTitle = book.stories.takeIf { it.size == 1 }?.firstOrNull()?.title
) )
} }
.values
.sortedBy {
it.bookTitle
}
} }
val books: State<List<AdventureBookUio>> val books: State<List<AdventureBookUio>>
@ -67,7 +63,6 @@ class AdventureBooksViewModel @Inject constructor(
_isLoading.value = true _isLoading.value = true
} }
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
adventureRepository.fetchBooks() adventureRepository.fetchBooks()
} }
} catch (_: Exception) { } catch (_: Exception) {

View file

@ -38,24 +38,24 @@ class AdventureStoriesViewModel @Inject constructor(
argument.bookTitle argument.bookTitle
} }
private val _chapters = adventureRepository.storyFlow(bookTitle = argument.bookTitle) private val _chapters = adventureRepository.bookFlow(documentId = argument.documentId)
.map { adventures -> .map { book ->
adventures book?.stories
.filter { it.bookTitle == argument.bookTitle && it.documentId == argument.documentId } ?.groupBy { it.category ?: "" }
.groupBy { it.storyCategory ?: "" } ?.flatMap { entry ->
.flatMap { entry ->
val header = listOf( val header = listOf(
AdventureStoriesUio.AdventureCategory(title = entry.key) AdventureStoriesUio.AdventureCategory(title = entry.key)
) )
val stories = entry.value.map { val stories = entry.value.map {
AdventureStoriesUio.AdventureItem( AdventureStoriesUio.AdventureItem(
documentId = it.documentId, documentId = book.documentId,
bookTitle = it.bookTitle, bookTitle = book.title,
adventureTitle = it.storyTitle, adventureTitle = it.title,
) )
} }
header + stories header + stories
} }
?: emptyList()
} }
val chapters: State<List<AdventureStoriesUio>> val chapters: State<List<AdventureStoriesUio>>

View file

@ -2,6 +2,7 @@ package com.pixelized.rplexicon.ui.screens.adventure.detail
import android.net.Uri import android.net.Uri
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
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
@ -10,7 +11,7 @@ import androidx.compose.runtime.remember
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.pixelized.rplexicon.data.model.adventure.AdventureLine.Format import com.pixelized.rplexicon.data.model.adventure.AdventureLineFormat
import com.pixelized.rplexicon.data.repository.adventure.AdventureRepository 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
@ -30,9 +31,9 @@ class AdventureDetailViewModel @Inject constructor(
) : ViewModel() { ) : ViewModel() {
private val argument = savedStateHandle.adventureDetailArgument private val argument = savedStateHandle.adventureDetailArgument
private val detail = adventureRepository.adventureFlow( private val detail = adventureRepository.storyFlow(
documentId = argument.documentId, documentId = argument.documentId,
adventureTitle = argument.adventureTitle, storyTitle = argument.adventureTitle,
).stateIn( ).stateIn(
scope = viewModelScope, scope = viewModelScope,
started = SharingStarted.Lazily, started = SharingStarted.Lazily,
@ -44,30 +45,30 @@ class AdventureDetailViewModel @Inject constructor(
val background: State<Uri?> val background: State<Uri?>
@Composable @Composable
@Stable
get() = remember(detail) { get() = remember(detail) {
detail.map { it?.storyBackground } detail.map { it?.background }
}.collectAsState(initial = null) }.collectAsState(initial = null)
val adventure: State<List<AdventureLineUio>> val adventure: State<List<AdventureLineUio>>
@Composable @Composable
get() = remember(detail) { get() = remember(detail) {
detail detail.map { adventure ->
.map { adventure -> adventure?.lines?.map { line ->
adventure?.story?.map { line -> AdventureLineUio(
AdventureLineUio( text = line.text,
text = line.text, style = when (line.format) {
style = when (line.format) { AdventureLineFormat.TITLE -> Style.TITLE
Format.TITLE -> Style.TITLE AdventureLineFormat.SUB_TITLE -> Style.SUB_TITLE
Format.SUB_TITLE -> Style.SUB_TITLE AdventureLineFormat.CHAPTER -> Style.CHAPTER
Format.CHAPTER -> Style.CHAPTER AdventureLineFormat.PARAGRAPH -> Style.PARAGRAPH
Format.PARAGRAPH -> Style.PARAGRAPH AdventureLineFormat.DIALOGUE -> Style.DIALOGUE
Format.DIALOGUE -> Style.DIALOGUE AdventureLineFormat.ANNEX -> Style.ANNEX
Format.ANNEX -> Style.ANNEX AdventureLineFormat.LEGEND -> Style.LEGEND
Format.LEGEND -> Style.LEGEND },
}, )
) } ?: emptyList()
} ?: emptyList() }
}
}.collectAsState(initial = emptyList()) }.collectAsState(initial = emptyList())
private val titleCell: State<AdventureLineUio?> private val titleCell: State<AdventureLineUio?>