diff --git a/app/src/main/java/com/pixelized/rplexicon/data/database/adventure/AdventureDao.kt b/app/src/main/java/com/pixelized/rplexicon/data/database/adventure/AdventureDao.kt index 31979a4..6140d8c 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/database/adventure/AdventureDao.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/database/adventure/AdventureDao.kt @@ -36,56 +36,67 @@ interface AdventureDao { @Query("SELECT * from ${AdventureLineDbo.TABLE} where documentId = :documentId AND title = :storyTitle") fun fetchAdventureLine(documentId: String, storyTitle: String): List - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertBook(book: AdventureBookDbo) + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insertBook(book: AdventureBookDbo): Long @Update fun updateBook(book: AdventureBookDbo) + fun insertOrUpdateBook(book: AdventureBookDbo) { + if (insertBook(book = book) == 1L) { + updateBook(book = book) + } + } + @Delete(entity = AdventureBookDbo::class) fun deleteBook(id: AdventureBookDbo.BookId) - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertStory(story: AdventureStoryDbo) + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insertStory(story: AdventureStoryDbo): Long @Update fun updateStory(story: AdventureStoryDbo) + fun insertOrUpdateStory(story: AdventureStoryDbo) { + if (insertStory(story = story) == 1L) { + updateStory(story = story) + } + } + @Delete(entity = AdventureStoryDbo::class) fun deleteStory(id: AdventureStoryDbo.StoryId) - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertLine(line: AdventureLineDbo) + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insertLine(line: AdventureLineDbo): Long @Update fun updateLine(line: AdventureLineDbo) + fun insertOrUpdateLine(line: AdventureLineDbo) { + if (insertLine(line = line) == 1L) { + updateLine(line = line) + } + } + @Delete(entity = AdventureLineDbo::class) fun deleteLines(id: AdventureLineDbo.LineId) @Transaction fun update( - booksToInsert: List, - booksToUpdate: List, - booksToRemove: List, - storiesToInsert: List, - storiesToUpdate: List, - storiesToRemove: List, - linesToInsert: List, - linesToUpdate: List, - linesToRemove: List, + booksToInsertOrUpdate: List = emptyList(), + booksToRemove: List = emptyList(), + storiesToInsertOrUpdate: List = emptyList(), + storiesToRemove: List = emptyList(), + linesToInsertOrUpdate: List = emptyList(), + linesToRemove: List = emptyList(), ) { // First remove the stuff linesToRemove.forEach { deleteLines(id = it) } storiesToRemove.forEach { deleteStory(id = it) } booksToRemove.forEach { deleteBook(id = it) } - // then insert the stuff - booksToInsert.forEach { insertBook(book = it) } - storiesToInsert.forEach { insertStory(story = it) } - linesToInsert.forEach { insertLine(line = it) } - // and update the stuff - booksToUpdate.forEach { updateBook(book = it) } - storiesToUpdate.forEach { updateStory(story = it) } - linesToUpdate.forEach { updateLine(line = it) } + // then insert or update the stuff + booksToInsertOrUpdate.forEach { insertOrUpdateBook(book = it) } + storiesToInsertOrUpdate.forEach { insertOrUpdateStory(story = it) } + linesToInsertOrUpdate.forEach { insertOrUpdateLine(line = it) } } } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/data/model/adventure/Adventure.kt b/app/src/main/java/com/pixelized/rplexicon/data/model/adventure/Adventure.kt deleted file mode 100644 index 80c1c59..0000000 --- a/app/src/main/java/com/pixelized/rplexicon/data/model/adventure/Adventure.kt +++ /dev/null @@ -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, -) \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/data/model/adventure/AdventureLine.kt b/app/src/main/java/com/pixelized/rplexicon/data/model/adventure/AdventureLine.kt index 42068f1..9bf7083 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/model/adventure/AdventureLine.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/model/adventure/AdventureLine.kt @@ -1,16 +1,6 @@ package com.pixelized.rplexicon.data.model.adventure data class AdventureLine( - val format: Format, + val format: AdventureLineFormat, val text: String, -) { - enum class Format { - TITLE, - SUB_TITLE, - CHAPTER, - PARAGRAPH, - DIALOGUE, - ANNEX, - LEGEND, - } -} +) diff --git a/app/src/main/java/com/pixelized/rplexicon/data/model/adventure/AdventureLineFormat.kt b/app/src/main/java/com/pixelized/rplexicon/data/model/adventure/AdventureLineFormat.kt new file mode 100644 index 0000000..75da908 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/data/model/adventure/AdventureLineFormat.kt @@ -0,0 +1,11 @@ +package com.pixelized.rplexicon.data.model.adventure + +enum class AdventureLineFormat { + TITLE, + SUB_TITLE, + CHAPTER, + PARAGRAPH, + DIALOGUE, + ANNEX, + LEGEND, +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/data/model/adventure/Book.kt b/app/src/main/java/com/pixelized/rplexicon/data/model/adventure/Book.kt new file mode 100644 index 0000000..3354105 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/data/model/adventure/Book.kt @@ -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, +) { + data class Story( + val title: String, + val category: String?, + val background: Uri?, + val lines: List, + ) { + data class Line( + val format: AdventureLineFormat, + val text: String, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/data/parser/adventure/AdventureStoryLineParser.kt b/app/src/main/java/com/pixelized/rplexicon/data/parser/adventure/AdventureStoryLineParser.kt index 8af4af2..18a94b8 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/parser/adventure/AdventureStoryLineParser.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/parser/adventure/AdventureStoryLineParser.kt @@ -2,6 +2,7 @@ package com.pixelized.rplexicon.data.parser.adventure import com.google.api.services.sheets.v4.model.ValueRange import com.pixelized.rplexicon.data.model.adventure.AdventureLine +import com.pixelized.rplexicon.data.model.adventure.AdventureLineFormat import com.pixelized.rplexicon.utilitary.extentions.sheet 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) { - TITLE -> AdventureLine.Format.TITLE - SUB_TITLE -> AdventureLine.Format.SUB_TITLE - CHAPTER -> AdventureLine.Format.CHAPTER - PARAGRAPH -> AdventureLine.Format.PARAGRAPH - DIALOGUE -> AdventureLine.Format.DIALOGUE - ANNEX -> AdventureLine.Format.ANNEX - LEGEND -> AdventureLine.Format.LEGEND + TITLE -> AdventureLineFormat.TITLE + SUB_TITLE -> AdventureLineFormat.SUB_TITLE + CHAPTER -> AdventureLineFormat.CHAPTER + PARAGRAPH -> AdventureLineFormat.PARAGRAPH + DIALOGUE -> AdventureLineFormat.DIALOGUE + ANNEX -> AdventureLineFormat.ANNEX + LEGEND -> AdventureLineFormat.LEGEND else -> null } diff --git a/app/src/main/java/com/pixelized/rplexicon/data/repository/adventure/AdventureDboFactory.kt b/app/src/main/java/com/pixelized/rplexicon/data/repository/adventure/AdventureDboFactory.kt index 0a665fd..7ef85d9 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/repository/adventure/AdventureDboFactory.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/repository/adventure/AdventureDboFactory.kt @@ -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.AdventureLineDbo 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.AdventureLine +import com.pixelized.rplexicon.data.model.adventure.AdventureLineFormat 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 javax.inject.Inject class AdventureDboFactory @Inject constructor() { fun convertFromDbo( + updatingBookId: String?, books: List, stories: List, lines: List, - ): List { - return books.flatMap { book: AdventureBookDbo -> - stories - .filter { story -> story.documentId == book.documentId } - .sortedBy { story -> story.index } - .map { story: AdventureStoryDbo -> - Adventure( - bookTitle = book.title, - bookIcon = book.icon.toUriOrNull(), - documentId = book.documentId, - storyTitle = story.title, - storyCategory = story.category, - storyBackground = story.background.toUriOrNull(), - story = lines - .filter { it.documentId == story.documentId && it.story == story.title } - .sortedBy { it.index } - .map { line -> - AdventureLine( - text = line.text, - format = try { - AdventureLine.Format.valueOf(line.format) - } catch (_: Exception) { - AdventureLine.Format.PARAGRAPH - }, - ) - }, - ) - } + ): List { + val indexedStories = stories.groupBy { it.documentId } + val indexedLines = lines.groupBy { it.documentId to it.story } + return books.map { book: AdventureBookDbo -> + Book( + documentId = book.documentId, + title = book.title, + icon = book.icon.toUriOrNull(), + updating = book.documentId == updatingBookId, + stories = indexedStories[book.documentId] + ?.sortedBy { story -> story.index } + ?.map { story -> + Book.Story( + title = story.title, + category = story.category, + background = story.background.toUriOrNull(), + lines = indexedLines[book.documentId to story.title] + ?.sortedBy { it.index } + ?.map { line -> + Book.Story.Line( + text = line.text, + format = try { + AdventureLineFormat.valueOf(line.format) + } catch (_: Exception) { + AdventureLineFormat.PARAGRAPH + }, + ) + } + ?: emptyList(), + ) + } + ?: emptyList(), + ) } } diff --git a/app/src/main/java/com/pixelized/rplexicon/data/repository/adventure/AdventureRepository.kt b/app/src/main/java/com/pixelized/rplexicon/data/repository/adventure/AdventureRepository.kt index 1428779..263a4112 100644 --- a/app/src/main/java/com/pixelized/rplexicon/data/repository/adventure/AdventureRepository.kt +++ b/app/src/main/java/com/pixelized/rplexicon/data/repository/adventure/AdventureRepository.kt @@ -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.AdventureLineDbo 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.AdventureStoryLineParser import com.pixelized.rplexicon.data.parser.adventure.AdventureStoryParser @@ -17,6 +17,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map @@ -24,7 +25,6 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext import javax.inject.Inject import javax.inject.Singleton -import kotlin.math.min @Singleton class AdventureRepository @Inject constructor( @@ -41,13 +41,16 @@ class AdventureRepository @Inject constructor( var lastSuccessFullUpdate: Update = Update.INITIAL private set + private val updatingBookId = MutableStateFlow(null) private val adventures = database.let { database -> combine( + updatingBookId, database.getBooksFlow(), database.getStoriesFlow(), database.getStoryLinesFlow(), - ) { books, stories, lines -> + ) { updatingBookId, books, stories, lines -> adventureDboFactory.convertFromDbo( + updatingBookId = updatingBookId, books = books, stories = stories, lines = lines, @@ -59,49 +62,112 @@ class AdventureRepository @Inject constructor( ) } - fun bookFlow(): Flow> { + fun booksFlow(): Flow> { return adventures } - fun storyFlow(bookTitle: String): Flow> { + fun bookFlow(documentId: String): Flow { return adventures.map { adventures -> - adventures.filter { adventure -> - adventure.bookTitle == bookTitle + adventures.firstOrNull { adventure -> + adventure.documentId == documentId } } } - fun adventureFlow(documentId: String, adventureTitle: String): Flow { + fun storyFlow(documentId: String, storyTitle: String): Flow { return adventures.map { adventures -> - adventures.firstOrNull { adventure -> - adventure.documentId == documentId && adventure.storyTitle == adventureTitle + adventures.firstNotNullOfOrNull { adventure -> + if (adventure.documentId == documentId) { + adventure.stories.firstOrNull { it.title == storyTitle } + } else { + null + } } } } @Throws(IncompatibleSheetStructure::class, Exception::class) - suspend fun fetchBooks() = update { - fetchAndCompareBooks() + suspend fun fetchBooks() { + val exceptions = mutableListOf() + val cache = database.fetchAdventureBooks() - (booksToInsert + booksToUpdate).forEach { book -> - fetchAndCompareStories(documentId = book.documentId) + try { + val update = service.fetchAdventureBooks().sortedBy { it.title } + + val booksToRemove = cache.map { it.id }.toMutableList() + val booksToInsertOrUpdate = mutableListOf() + + 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 } - - (storiesToInsert + storiesToUpdate).forEach { story -> - fetchAndCompareLines(documentId = story.documentId, storyTitle = story.title) + if (exceptions.isNotEmpty()) { + throw exceptions.first() } - - lastSuccessFullUpdate = Update.currentTime() } @Throws(IncompatibleSheetStructure::class, Exception::class) suspend fun fetchStories( documentId: String, ) = update { - fetchAndCompareStories(documentId = documentId) + val exceptions = mutableListOf() + try { + updatingBookId.value = documentId + fetchAndCompareStories(documentId = documentId) - (storiesToInsert + storiesToUpdate).forEach { story -> - fetchAndCompareLines(documentId = story.documentId, storyTitle = story.title) + (storiesToInsertOrUpdate).forEach { story -> + 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, storyTitle: String, ) = update { - // specific case for adventure fetching, need to update the story too as the background is stored there. - service.fetchAdventureStories( - documentId = documentId, - ).firstOrNull { story -> - story.documentId == documentId && story.title == storyTitle - }?.let { story -> - storiesToUpdate.add(story) - } - - fetchAndCompareLines(documentId = documentId, storyTitle = storyTitle) - } - - private suspend fun DatabaseUpdateScope.fetchAndCompareBooks() { - val cache = database.fetchAdventureBooks() - val update = service.fetchAdventureBooks() - - val toRemove = cache.map { it.id }.toMutableList() - val toInsert = mutableListOf() - val toUpdate = mutableListOf() - - update.forEach { item -> - when (toRemove.remove(item.id)) { - true -> toUpdate.add(item) - else -> toInsert.add(item) + val exceptions = mutableListOf() + try { + updatingBookId.value = documentId + // specific case for adventure fetching, need to update the story too as the background is stored there. + service.fetchAdventureStories( + documentId = documentId, + ).firstOrNull { story -> + story.documentId == documentId && story.title == storyTitle + }?.let { story -> + storiesToInsertOrUpdate.add(story) } - } - booksToInsert.addAll(elements = toInsert) - booksToUpdate.addAll(elements = toUpdate) - booksToRemove.addAll(elements = toRemove) + try { + fetchAndCompareLines(documentId = documentId, storyTitle = storyTitle) + } 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( @@ -151,7 +212,7 @@ class AdventureRepository @Inject constructor( ): Boolean { return cache?.let { it.revision < item.revision || cache.index != item.index - } ?: false + } ?: true } val cache = database.fetchAdventureStories( @@ -164,49 +225,39 @@ class AdventureRepository @Inject constructor( ) val toRemove = cache.keys.toMutableList() - val toInsert = mutableListOf() - val toUpdate = mutableListOf() + val toInsertOrUpdate = mutableListOf() update.forEach { item -> - when (toRemove.remove(item.id)) { - true -> if (shouldUpdate(cache = cache[item.id], item = item)) toUpdate.add(item) - else -> toInsert.add(item) + toRemove.remove(item.id) + if (shouldUpdate(cache = cache[item.id], item = item)) { + toInsertOrUpdate.add(item) } } - storiesToInsert.addAll(elements = toInsert) - storiesToUpdate.addAll(elements = toUpdate) storiesToRemove.addAll(elements = toRemove) + storiesToInsertOrUpdate.addAll(elements = toInsertOrUpdate) } private suspend fun DatabaseUpdateScope.fetchAndCompareLines( documentId: String, storyTitle: String, ) { - val cache = database.fetchAdventureLine( + val cache: List = database.fetchAdventureLine( documentId = documentId, storyTitle = storyTitle, ) - val update = service.fetchAdventureLine( + val update: List = service.fetchAdventureLine( documentId = documentId, storyTitle = storyTitle, ) - // if cache is smaller than the update we need to insert - 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 cache is bigger than the update we need to delete the excess lines. if (update.size < cache.size) { (update.size until cache.size).forEach { linesToRemove.add(cache[it].id) } } - // then we update the rest - (0 until min(cache.size, update.size)).forEach { - linesToUpdate.add(update[it]) - } + // then we insert / update + linesToInsertOrUpdate.addAll(update) } private suspend inline fun update( @@ -217,27 +268,21 @@ class AdventureRepository @Inject constructor( lambda.invoke(update) database.update( - booksToInsert = update.booksToInsert, - booksToUpdate = update.booksToUpdate, + booksToInsertOrUpdate = update.booksToInsertOrUpdate, booksToRemove = update.booksToRemove, - storiesToInsert = update.storiesToInsert, - storiesToUpdate = update.storiesToUpdate, + storiesToInsertOrUpdate = update.storiesToInsertOrUpdate, storiesToRemove = update.storiesToRemove, - linesToInsert = update.linesToInsert, - linesToUpdate = update.linesToUpdate, + linesToInsertOrUpdate = update.linesToInsertOrUpdate, linesToRemove = update.linesToRemove, ) } private data class DatabaseUpdateScope( - val booksToInsert: MutableList = mutableListOf(), - val booksToUpdate: MutableList = mutableListOf(), + val booksToInsertOrUpdate: MutableList = mutableListOf(), val booksToRemove: MutableList = mutableListOf(), - val storiesToInsert: MutableList = mutableListOf(), - val storiesToUpdate: MutableList = mutableListOf(), + val storiesToInsertOrUpdate: MutableList = mutableListOf(), val storiesToRemove: MutableList = mutableListOf(), - val linesToInsert: MutableList = mutableListOf(), - val linesToUpdate: MutableList = mutableListOf(), + val linesToInsertOrUpdate: MutableList = mutableListOf(), val linesToRemove: MutableList = mutableListOf(), ) @@ -270,13 +315,9 @@ class AdventureRepository @Inject constructor( documentId: String, storyTitle: String, ): List { - return try { - googleRepository.fetch { sheet -> - val request = sheet.get(documentId, storyTitle) - adventureStoryLineParser.parse(sheet = request.execute()) - } - } catch (exception: Exception) { - emptyList() + return googleRepository.fetch { sheet -> + val request = sheet.get(documentId, storyTitle) + adventureStoryLineParser.parse(sheet = request.execute()) }.mapIndexed { index, line -> adventureDboFactory.convertToDbo( adventureLine = line, diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/book/AdventureBook.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/book/AdventureBook.kt index 26ab2d8..ae42666 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/book/AdventureBook.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/book/AdventureBook.kt @@ -1,10 +1,12 @@ package com.pixelized.rplexicon.ui.screens.adventure.book import android.content.res.Configuration -import android.net.Uri +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box 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.shape.RoundedCornerShape 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.text.style.TextAlign 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 com.pixelized.rplexicon.R import com.pixelized.rplexicon.ui.composable.images.BackgroundImage 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.utilitary.annotateWithDropCap import com.pixelized.rplexicon.utilitary.extentions.lexicon @@ -34,6 +39,7 @@ data class AdventureBookUio( val bookIcon: Any?, val documentId: String, val adventureTitle: String?, + val updating: Boolean, ) @Composable @@ -47,14 +53,19 @@ fun AdventureBook( Box( modifier = Modifier .clip(shape = shape) - .clickable(onClick = onClick) + .clickable(enabled = item.updating.not(), onClick = onClick) .then(other = modifier), ) { BackgroundImage( modifier = Modifier .matchParentSize() .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), contentScale = ContentScale.FillWidth, alignment = Alignment.TopCenter, @@ -77,18 +88,38 @@ fun AdventureBook( @Composable @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun AdventureItemPreview() { +private fun AdventureItemPreview( + @PreviewParameter(AdventureItemProvider::class) preview: AdventureBookUio, +) { LexiconTheme { Surface { AdventureBook( - item = AdventureBookUio( - bookIcon = R.drawable.icbg_book_generic_c_unfaded, - bookTitle = "Les chroniques d'une orc", - documentId = "", - adventureTitle = null, - ), + modifier = Modifier + .fillMaxWidth(0.4f) + .aspectRatio(ratio = 0.8f), + item = preview, onClick = {}, ) } } +} + +private class AdventureItemProvider : PreviewParameterProvider { + override val values: Sequence = 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, + ), + ) + } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/book/AdventureBooksScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/book/AdventureBooksScreen.kt index d6e906e..8c6dc41 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/book/AdventureBooksScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/book/AdventureBooksScreen.kt @@ -177,8 +177,23 @@ private fun AdventureListPreview( bookTitle = "Les chroniques d'une orc", bookIcon = null, documentId = "", + updating = true, 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, + ), ) ) }, diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/book/AdventureBooksViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/book/AdventureBooksViewModel.kt index 3719dce..623d1be 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/book/AdventureBooksViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/book/AdventureBooksViewModel.kt @@ -29,23 +29,19 @@ class AdventureBooksViewModel @Inject constructor( private val _isLoading = mutableStateOf(false) val isLoading: State get() = _isLoading - private val _books = adventureRepository.bookFlow() + private val _books = adventureRepository.booksFlow() .map { adventures -> adventures - .groupBy { it.documentId } - .mapValues { (_, adventures) -> - val adventure = adventures.first() + .sortedBy { it.title } + .map { book -> AdventureBookUio( - bookTitle = adventure.bookTitle, - bookIcon = adventure.bookIcon, - documentId = adventure.documentId, - adventureTitle = if (adventures.size == 1) adventure.storyTitle else null + bookTitle = book.title, + bookIcon = book.icon, + documentId = book.documentId, + updating = book.updating, + adventureTitle = book.stories.takeIf { it.size == 1 }?.firstOrNull()?.title ) } - .values - .sortedBy { - it.bookTitle - } } val books: State> @@ -67,7 +63,6 @@ class AdventureBooksViewModel @Inject constructor( _isLoading.value = true } withContext(Dispatchers.Default) { - adventureRepository.fetchBooks() } } catch (_: Exception) { diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/chapter/AdventureStoriesViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/chapter/AdventureStoriesViewModel.kt index d4f8926..b42bd95 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/chapter/AdventureStoriesViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/chapter/AdventureStoriesViewModel.kt @@ -38,24 +38,24 @@ class AdventureStoriesViewModel @Inject constructor( argument.bookTitle } - private val _chapters = adventureRepository.storyFlow(bookTitle = argument.bookTitle) - .map { adventures -> - adventures - .filter { it.bookTitle == argument.bookTitle && it.documentId == argument.documentId } - .groupBy { it.storyCategory ?: "" } - .flatMap { entry -> + private val _chapters = adventureRepository.bookFlow(documentId = argument.documentId) + .map { book -> + book?.stories + ?.groupBy { it.category ?: "" } + ?.flatMap { entry -> val header = listOf( AdventureStoriesUio.AdventureCategory(title = entry.key) ) val stories = entry.value.map { AdventureStoriesUio.AdventureItem( - documentId = it.documentId, - bookTitle = it.bookTitle, - adventureTitle = it.storyTitle, + documentId = book.documentId, + bookTitle = book.title, + adventureTitle = it.title, ) } header + stories } + ?: emptyList() } val chapters: State> diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/detail/AdventureDetailViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/detail/AdventureDetailViewModel.kt index 51b3567..fa5c672 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/detail/AdventureDetailViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/adventure/detail/AdventureDetailViewModel.kt @@ -2,6 +2,7 @@ package com.pixelized.rplexicon.ui.screens.adventure.detail import android.net.Uri 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 @@ -10,7 +11,7 @@ import androidx.compose.runtime.remember import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel 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.ui.navigation.screens.adventureDetailArgument import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureLineUio.Style @@ -30,9 +31,9 @@ class AdventureDetailViewModel @Inject constructor( ) : ViewModel() { private val argument = savedStateHandle.adventureDetailArgument - private val detail = adventureRepository.adventureFlow( + private val detail = adventureRepository.storyFlow( documentId = argument.documentId, - adventureTitle = argument.adventureTitle, + storyTitle = argument.adventureTitle, ).stateIn( scope = viewModelScope, started = SharingStarted.Lazily, @@ -44,30 +45,30 @@ class AdventureDetailViewModel @Inject constructor( val background: State @Composable + @Stable get() = remember(detail) { - detail.map { it?.storyBackground } + detail.map { it?.background } }.collectAsState(initial = null) val adventure: State> @Composable get() = remember(detail) { - detail - .map { adventure -> - adventure?.story?.map { line -> - AdventureLineUio( - text = line.text, - style = when (line.format) { - Format.TITLE -> Style.TITLE - Format.SUB_TITLE -> Style.SUB_TITLE - Format.CHAPTER -> Style.CHAPTER - Format.PARAGRAPH -> Style.PARAGRAPH - Format.DIALOGUE -> Style.DIALOGUE - Format.ANNEX -> Style.ANNEX - Format.LEGEND -> Style.LEGEND - }, - ) - } ?: emptyList() - } + detail.map { adventure -> + adventure?.lines?.map { line -> + AdventureLineUio( + text = line.text, + style = when (line.format) { + AdventureLineFormat.TITLE -> Style.TITLE + AdventureLineFormat.SUB_TITLE -> Style.SUB_TITLE + AdventureLineFormat.CHAPTER -> Style.CHAPTER + AdventureLineFormat.PARAGRAPH -> Style.PARAGRAPH + AdventureLineFormat.DIALOGUE -> Style.DIALOGUE + AdventureLineFormat.ANNEX -> Style.ANNEX + AdventureLineFormat.LEGEND -> Style.LEGEND + }, + ) + } ?: emptyList() + } }.collectAsState(initial = emptyList()) private val titleCell: State