Refactor the adventure database to better fit the model.

This commit is contained in:
Andres Gomez, Thomas (ITDV RL) 2024-06-19 22:40:39 +02:00
parent 96269cf84a
commit b6489e5a85
15 changed files with 497 additions and 337 deletions

View file

@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 2, "version": 2,
"identityHash": "eecd0da0c8ae6578a5d36c3b926c2fe8", "identityHash": "95cb578b3b61a022ab4dda676d2e4645",
"entities": [ "entities": [
{ {
"tableName": "lexicon", "tableName": "lexicon",
@ -320,51 +320,71 @@
"foreignKeys": [] "foreignKeys": []
}, },
{ {
"tableName": "adventures", "tableName": "AdventureBooks",
"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`))", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`documentId` TEXT NOT NULL, `title` TEXT NOT NULL, `icon` TEXT, PRIMARY KEY(`documentId`))",
"fields": [ "fields": [
{ {
"fieldPath": "bookTitle", "fieldPath": "documentId",
"columnName": "bookTitle", "columnName": "documentId",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "adventureTitle", "fieldPath": "title",
"columnName": "adventureTitle", "columnName": "title",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "adventureCategory", "fieldPath": "icon",
"columnName": "adventureCategory", "columnName": "icon",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": false "notNull": false
}, }
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"documentId"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "AdventureStory",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`title` TEXT NOT NULL, `category` TEXT, `background` TEXT, `revision` INTEGER NOT NULL, `index` INTEGER NOT NULL, `documentId` TEXT NOT NULL, PRIMARY KEY(`title`, `documentId`), FOREIGN KEY(`documentId`) REFERENCES `AdventureBooks`(`documentId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{ {
"fieldPath": "bookIcon", "fieldPath": "title",
"columnName": "bookIcon", "columnName": "title",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "adventureBackground",
"columnName": "adventureBackground",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "index",
"columnName": "index",
"affinity": "INTEGER",
"notNull": true "notNull": true
}, },
{
"fieldPath": "category",
"columnName": "category",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "background",
"columnName": "background",
"affinity": "TEXT",
"notNull": false
},
{ {
"fieldPath": "revision", "fieldPath": "revision",
"columnName": "revision", "columnName": "revision",
"affinity": "INTEGER", "affinity": "INTEGER",
"notNull": true "notNull": true
}, },
{
"fieldPath": "index",
"columnName": "index",
"affinity": "INTEGER",
"notNull": true
},
{ {
"fieldPath": "documentId", "fieldPath": "documentId",
"columnName": "documentId", "columnName": "documentId",
@ -375,29 +395,29 @@
"primaryKey": { "primaryKey": {
"autoGenerate": false, "autoGenerate": false,
"columnNames": [ "columnNames": [
"bookTitle", "title",
"adventureTitle" "documentId"
] ]
}, },
"indices": [], "indices": [],
"foreignKeys": [] "foreignKeys": [
{
"table": "AdventureBooks",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"documentId"
],
"referencedColumns": [
"documentId"
]
}
]
}, },
{ {
"tableName": "adventuresStories", "tableName": "AdventureLine",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookTitle` TEXT NOT NULL, `adventureTitle` TEXT NOT NULL, `index` INTEGER NOT NULL, `text` TEXT NOT NULL, `format` TEXT NOT NULL, PRIMARY KEY(`bookTitle`, `adventureTitle`, `index`), FOREIGN KEY(`bookTitle`, `adventureTitle`) REFERENCES `adventures`(`bookTitle`, `adventureTitle`) ON UPDATE NO ACTION ON DELETE CASCADE )", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`index` INTEGER NOT NULL, `text` TEXT NOT NULL, `format` TEXT NOT NULL, `documentId` TEXT NOT NULL, `title` TEXT NOT NULL, PRIMARY KEY(`index`, `title`, `documentId`), FOREIGN KEY(`title`, `documentId`) REFERENCES `AdventureStory`(`title`, `documentId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [ "fields": [
{
"fieldPath": "bookTitle",
"columnName": "bookTitle",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "adventureTitle",
"columnName": "adventureTitle",
"affinity": "TEXT",
"notNull": true
},
{ {
"fieldPath": "index", "fieldPath": "index",
"columnName": "index", "columnName": "index",
@ -415,29 +435,41 @@
"columnName": "format", "columnName": "format",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": true "notNull": true
},
{
"fieldPath": "documentId",
"columnName": "documentId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "story",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
} }
], ],
"primaryKey": { "primaryKey": {
"autoGenerate": false, "autoGenerate": false,
"columnNames": [ "columnNames": [
"bookTitle", "index",
"adventureTitle", "title",
"index" "documentId"
] ]
}, },
"indices": [], "indices": [],
"foreignKeys": [ "foreignKeys": [
{ {
"table": "adventures", "table": "AdventureStory",
"onDelete": "CASCADE", "onDelete": "CASCADE",
"onUpdate": "NO ACTION", "onUpdate": "NO ACTION",
"columns": [ "columns": [
"bookTitle", "title",
"adventureTitle" "documentId"
], ],
"referencedColumns": [ "referencedColumns": [
"bookTitle", "title",
"adventureTitle" "documentId"
] ]
} }
] ]
@ -446,7 +478,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, 'eecd0da0c8ae6578a5d36c3b926c2fe8')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '95cb578b3b61a022ab4dda676d2e4645')"
] ]
} }
} }

View file

@ -5,8 +5,9 @@ import androidx.room.AutoMigration
import androidx.room.Database import androidx.room.Database
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import com.pixelized.rplexicon.data.database.adventure.AdventureBookDbo
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.AdventureLineDbo
import com.pixelized.rplexicon.data.database.adventure.AdventureStoryDbo import com.pixelized.rplexicon.data.database.adventure.AdventureStoryDbo
import com.pixelized.rplexicon.data.database.lexicon.LexiconDao import com.pixelized.rplexicon.data.database.lexicon.LexiconDao
import com.pixelized.rplexicon.data.database.lexicon.LexiconDbo import com.pixelized.rplexicon.data.database.lexicon.LexiconDbo
@ -28,8 +29,9 @@ import dagger.hilt.components.SingletonComponent
QuestDbo::class, QuestDbo::class,
LocationDbo::class, LocationDbo::class,
WorldDbo::class, WorldDbo::class,
AdventureDbo::class, AdventureBookDbo::class,
AdventureStoryDbo::class, AdventureStoryDbo::class,
AdventureLineDbo::class,
], ],
version = 2, version = 2,
exportSchema = true, exportSchema = true,

View file

@ -11,71 +11,74 @@ import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface AdventureDao { interface AdventureDao {
@Query("SELECT * from adventures") @Query("SELECT * from ${AdventureBookDbo.TABLE}")
fun getAllAdventureFlow(): Flow<List<AdventureDbo>> fun getBooksFlow(): Flow<List<AdventureBookDbo>>
@Query("SELECT * from adventuresStories") @Query("SELECT * from ${AdventureStoryDbo.TABLE}")
fun getAllAdventureStoryFlow(): Flow<List<AdventureStoryDbo>> fun getStoriesFlow(): Flow<List<AdventureStoryDbo>>
@Query("SELECT * from adventures") @Query("SELECT * from ${AdventureLineDbo.TABLE}")
fun findAdventures(): List<AdventureDbo> fun getStoryLinesFlow(): Flow<List<AdventureLineDbo>>
@Query("SELECT * from adventures where bookTitle = :bookTitle") @Query("SELECT * from ${AdventureBookDbo.TABLE}")
fun findAdventures(bookTitle: String): List<AdventureDbo> fun findBooks(): List<AdventureBookDbo>
@Query("SELECT * from adventures where bookTitle = :bookTitle and adventureTitle = :adventureTitle") @Query("SELECT * from ${AdventureStoryDbo.TABLE}")
fun findAdventure(bookTitle: String, adventureTitle: String): AdventureDbo? fun findStories(): List<AdventureStoryDbo>
@Query("SELECT * from ${AdventureStoryDbo.TABLE} where documentId = :documentId")
fun findStories(documentId: String): List<AdventureStoryDbo>
@Query("SELECT * from ${AdventureStoryDbo.TABLE} where documentId = :documentId AND title = :title")
fun findStory(documentId: String, title: String): AdventureStoryDbo?
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertBook(book: AdventureBookDbo)
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertBook(item: AdventureDbo) fun insertStory(story: AdventureStoryDbo)
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertStory(item: AdventureStoryDbo) fun insertLines(line: AdventureLineDbo)
@Delete(entity = AdventureDbo::class) @Delete(entity = AdventureBookDbo::class)
fun deleteBook(id: BookPartialId) fun deleteBook(id: AdventureBookDbo.BookId)
@Delete(entity = AdventureDbo::class)
fun deleteBook(id: StoryPartialId)
@Delete(entity = AdventureStoryDbo::class) @Delete(entity = AdventureStoryDbo::class)
fun deleteStory(id: BookPartialId) fun deleteStory(id: AdventureBookDbo.BookId)
@Delete(entity = AdventureStoryDbo::class) @Delete(entity = AdventureStoryDbo::class)
fun deleteStory(id: StoryPartialId) fun deleteStory(id: AdventureStoryDbo.StoryId)
data class BookPartialId( @Delete(entity = AdventureLineDbo::class)
val bookTitle: String, fun deleteLines(id: AdventureLineDbo.LineId)
)
data class StoryPartialId(
val bookTitle: String,
val adventureTitle: String,
)
@Transaction @Transaction
fun update( fun update(
booksToRemove: List<BookPartialId>, books: List<AdventureBookDbo>,
storiesToRemove: List<StoryPartialId>, booksToRemove: List<AdventureBookDbo.BookId>,
adventure: List<Pair<AdventureDbo, List<AdventureStoryDbo>>> stories: List<AdventureStoryDbo>,
storiesToRemove: List<AdventureStoryDbo.StoryId>,
lines: List<AdventureLineDbo>,
linesToRemove: List<AdventureLineDbo.LineId>,
) { ) {
// First clean the database from old unused data. // First clean the database from old unused data.
// story lines are remove with cascading foreign key. booksToRemove.forEach {
booksToRemove.forEach { deleteBook(id = it) } deleteStory(id = it)
storiesToRemove.forEach { deleteBook(id = it) } deleteBook(id = it)
// then update the data. }
adventure.forEach { (adventure, lines) -> // StoryLineDbo are remove with cascading foreign key from StoryBdo.
// then remove the story lines before inserting them again (easier than to keep track of them all) storiesToRemove.forEach { deleteStory(id = it) }
deleteStory( linesToRemove.forEach { deleteLines(id = it) }
id = StoryPartialId(
bookTitle = adventure.bookTitle, books.forEach { book ->
adventureTitle = adventure.adventureTitle, insertBook(book = book)
) }
) stories.forEach { story ->
// insert the adventure insertStory(story = story)
insertBook(item = adventure) }
// insert its lines. lines.forEach { line ->
lines.forEach { insertStory(item = it) } insertLines(line = line)
} }
} }
} }

View file

@ -1,39 +1,133 @@
package com.pixelized.rplexicon.data.database.adventure package com.pixelized.rplexicon.data.database.adventure
import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import androidx.room.Ignore
@Entity( @Entity(
tableName = "adventures", tableName = AdventureBookDbo.TABLE,
primaryKeys = ["bookTitle", "adventureTitle"], primaryKeys = [AdventureBookDbo.DOCUMENT_ID],
) )
data class AdventureDbo( data class AdventureBookDbo(
val bookTitle: String, @ColumnInfo(name = DOCUMENT_ID)
val adventureTitle: String,
val adventureCategory: String?,
val bookIcon: String?,
val adventureBackground: String?,
val index: Int,
val revision: Long,
val documentId: String, val documentId: String,
) @ColumnInfo(name = TITLE)
val title: String,
@ColumnInfo(name = ICON)
val icon: String?,
) {
@Ignore
val id = BookId(documentId = documentId)
data class BookId(
@ColumnInfo(name = DOCUMENT_ID)
val documentId: String,
)
companion object {
const val TABLE = "AdventureBooks"
const val DOCUMENT_ID = "documentId"
const val TITLE = "title"
const val ICON = "icon"
}
}
@Entity( @Entity(
tableName = "adventuresStories", tableName = AdventureStoryDbo.TABLE,
primaryKeys = ["bookTitle", "adventureTitle", "index"], primaryKeys = [
AdventureStoryDbo.TITLE,
AdventureStoryDbo.FK_DOCUMENT_ID,
],
foreignKeys = [ foreignKeys = [
ForeignKey( ForeignKey(
entity = AdventureDbo::class, entity = AdventureBookDbo::class,
parentColumns = ["bookTitle", "adventureTitle"], parentColumns = [AdventureBookDbo.DOCUMENT_ID],
childColumns = ["bookTitle", "adventureTitle"], childColumns = [AdventureStoryDbo.FK_DOCUMENT_ID],
onDelete = ForeignKey.CASCADE, onDelete = ForeignKey.NO_ACTION,
), )
], ],
) )
data class AdventureStoryDbo( data class AdventureStoryDbo(
val bookTitle: String, @ColumnInfo(name = TITLE)
val adventureTitle: String, val title: String,
@ColumnInfo(name = CATEGORY)
val category: String?,
@ColumnInfo(name = BACKGROUND)
val background: String?,
@ColumnInfo(name = REVISION)
val revision: Long,
@ColumnInfo(name = INDEX)
val index: Int, val index: Int,
@ColumnInfo(name = FK_DOCUMENT_ID)
val documentId: String,
) {
@Ignore
val id = StoryId(documentId = documentId, title = title)
data class StoryId(
@ColumnInfo(name = FK_DOCUMENT_ID)
val documentId: String,
@ColumnInfo(name = TITLE)
val title: String,
)
companion object {
const val TABLE = "AdventureStory"
const val TITLE = "title"
const val CATEGORY = "category"
const val BACKGROUND = "background"
const val REVISION = "revision"
const val INDEX = "index"
const val FK_DOCUMENT_ID = AdventureBookDbo.DOCUMENT_ID
}
}
@Entity(
tableName = AdventureLineDbo.TABLE,
primaryKeys = [
AdventureLineDbo.INDEX,
AdventureLineDbo.FK_STORY_TITLE,
AdventureLineDbo.FK_DOCUMENT_ID,
],
foreignKeys = [
ForeignKey(
entity = AdventureStoryDbo::class,
parentColumns = [AdventureStoryDbo.TITLE, AdventureStoryDbo.FK_DOCUMENT_ID],
childColumns = [AdventureLineDbo.FK_STORY_TITLE, AdventureLineDbo.FK_DOCUMENT_ID],
onDelete = ForeignKey.CASCADE,
)
],
)
data class AdventureLineDbo(
@ColumnInfo(name = INDEX)
val index: Int,
@ColumnInfo(name = TEXT)
val text: String, val text: String,
@ColumnInfo(name = FORMAT)
val format: String, val format: String,
) @ColumnInfo(name = FK_DOCUMENT_ID)
val documentId: String,
@ColumnInfo(name = FK_STORY_TITLE)
val story: String,
) {
@Ignore
val id = LineId(documentId = documentId, story = story)
data class LineId(
@ColumnInfo(name = FK_DOCUMENT_ID)
val documentId: String,
@ColumnInfo(name = FK_STORY_TITLE)
val story: String,
)
companion object {
const val TABLE = "AdventureLine"
const val INDEX = "index"
const val TEXT = "text"
const val FORMAT = "format"
const val FK_DOCUMENT_ID = AdventureBookDbo.DOCUMENT_ID
const val FK_STORY_TITLE = AdventureStoryDbo.TITLE
}
}

View file

@ -5,9 +5,9 @@ import android.net.Uri
data class Adventure( data class Adventure(
val bookTitle: String, val bookTitle: String,
val bookIcon: Uri?, val bookIcon: Uri?,
val adventureCategory: String?, val storyCategory: String?,
val adventureTitle: String, val storyTitle: String,
val adventureBackground: Uri?, val storyBackground: Uri?,
val documentId: String, val documentId: String,
val story: List<AdventureLine>, val story: List<AdventureLine>,
) )

View file

@ -1,6 +1,7 @@
package com.pixelized.rplexicon.data.repository.adventure package com.pixelized.rplexicon.data.repository.adventure
import com.pixelized.rplexicon.data.database.adventure.AdventureDbo 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.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
@ -12,65 +13,71 @@ import javax.inject.Inject
class AdventureDboFactory @Inject constructor() { class AdventureDboFactory @Inject constructor() {
fun convertFromDbo( fun convertFromDbo(
adventures: List<AdventureDbo>, books: List<AdventureBookDbo>,
stories: List<AdventureStoryDbo>, stories: List<AdventureStoryDbo>,
lines: List<AdventureLineDbo>,
): List<Adventure> { ): List<Adventure> {
return adventures return books.flatMap { book: AdventureBookDbo ->
.sortedBy { it.index } stories
.map { adventure -> .filter { story -> story.documentId == book.documentId }
Adventure( .sortedBy { story -> story.index }
bookTitle = adventure.bookTitle, .map { story: AdventureStoryDbo ->
bookIcon = adventure.bookIcon.toUriOrNull(), Adventure(
adventureTitle = adventure.adventureTitle, bookTitle = book.title,
adventureCategory = adventure.adventureCategory, bookIcon = book.icon.toUriOrNull(),
adventureBackground = adventure.adventureBackground.toUriOrNull(), documentId = book.documentId,
documentId = adventure.documentId, storyTitle = story.title,
story = stories storyCategory = story.category,
.filter { storyBackground = story.background.toUriOrNull(),
it.bookTitle == adventure.bookTitle && it.adventureTitle == adventure.adventureTitle story = lines
} .filter { it.documentId == story.documentId && it.story == story.title }
.sortedBy { .sortedBy { it.index }
it.index .map { line ->
} AdventureLine(
.map { story -> text = line.text,
AdventureLine( format = try {
text = story.text, AdventureLine.Format.valueOf(line.format)
format = try { } catch (_: Exception) {
AdventureLine.Format.valueOf(story.format) AdventureLine.Format.PARAGRAPH
} catch (_: Exception) { },
AdventureLine.Format.PARAGRAPH )
}, },
) )
}, }
) }
}
} }
fun convertToAdventureDbo( fun convertToDbo(
book: AdventureBook, book: AdventureBook,
story: AdventureStory, ) = AdventureBookDbo(
index: Int,
) = AdventureDbo(
bookTitle = book.title,
adventureTitle = story.title,
adventureCategory = story.category,
bookIcon = book.icon.toString(),
adventureBackground = story.background.toString(),
index = index,
revision = story.revision,
documentId = book.documentId, documentId = book.documentId,
title = book.title,
icon = book.icon.toString(),
) )
fun convertToStoryDbo( fun convertToDbo(
book: AdventureBook,
story: AdventureStory, story: AdventureStory,
index: Int, index: Int,
line: AdventureLine documentId: String,
) = AdventureStoryDbo( ) = AdventureStoryDbo(
bookTitle = book.title, title = story.title,
adventureTitle = story.title, category = story.category,
background = story.background.toString(),
revision = story.revision,
index = index, index = index,
text = line.text, documentId = documentId,
format = line.format.name, )
fun convertToDbo(
adventureLine: AdventureLine,
index: Int,
documentId: String,
storyTitle: String,
) = AdventureLineDbo(
index = index,
text = adventureLine.text,
format = adventureLine.format.name,
documentId = documentId,
story = storyTitle,
) )
} }

View file

@ -1,8 +1,8 @@
package com.pixelized.rplexicon.data.repository.adventure 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.AdventureBookDbo
import com.pixelized.rplexicon.data.database.adventure.AdventureDbo 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.Adventure
import com.pixelized.rplexicon.data.model.adventure.AdventureBook import com.pixelized.rplexicon.data.model.adventure.AdventureBook
@ -32,123 +32,151 @@ class AdventureRepository @Inject constructor(
private val adventureStoryLineParser: AdventureStoryLineParser, private val adventureStoryLineParser: AdventureStoryLineParser,
private val adventureDboFactory: AdventureDboFactory, private val adventureDboFactory: AdventureDboFactory,
) { ) {
val adventure = database.adventureDao().getAllAdventureFlow() val adventure = database.adventureDao().let { database ->
.combine( combine(
flow = database.adventureDao().getAllAdventureStoryFlow(), database.getBooksFlow(),
transform = adventureDboFactory::convertFromDbo, database.getStoriesFlow(),
).stateIn( database.getStoryLinesFlow(),
) { books, stories, lines ->
adventureDboFactory.convertFromDbo(
books = books,
stories = stories,
lines = lines,
)
}.stateIn(
scope = CoroutineScope(Dispatchers.Default + Job()), scope = CoroutineScope(Dispatchers.Default + Job()),
started = SharingStarted.Lazily, started = SharingStarted.Lazily,
initialValue = emptyList(), initialValue = emptyList(),
) )
}
@Throws(IncompatibleSheetStructure::class, Exception::class) @Throws(IncompatibleSheetStructure::class, Exception::class)
suspend fun fetchAdventures() { suspend fun fetchBooks() {
val database = database.adventureDao() val database = database.adventureDao()
val bookStore = database.findAdventures()
val bookToRemove: MutableMap<String, Boolean> = bookStore.associate { val booksToRemove = database.findBooks()
it.bookTitle to true .map { it.id }
}.toMutableMap() .toMutableList()
val storyToRemove: MutableMap<Pair<String, String>, Boolean> = bookStore.associate { val books: List<AdventureBookDbo> = fetchAdventureBooks().map { book ->
(it.bookTitle to it.adventureTitle) to true // convert to BookDbo.
}.toMutableMap() adventureDboFactory.convertToDbo(book = book).also {
// Flag this book to not delete it
booksToRemove.remove(element = it.id)
}
}
val adventures = fetchAdventureBooks().flatMap { book -> val storiesToRemove = books
// tag this book to keep it in the database .flatMap { book -> database.findStories(documentId = book.documentId) }
bookToRemove[book.title] = false .map { it.id }
.toMutableList()
fetchAdventuresContent( val stories: List<AdventureStoryDbo> = books.flatMap { book ->
database = database, fetchAdventureStory(documentId = book.documentId)
storyToRemove = storyToRemove, .mapIndexed { index, story ->
book = book, // convert to StoryBdo
) val update = adventureDboFactory.convertToDbo(
story = story,
index = index,
documentId = book.documentId,
).also {
// Flag this story to not delete it
storiesToRemove.remove(element = it.id)
}
val cache = database.findStory(
documentId = book.documentId,
title = story.title,
)
when {
cache == null -> update
cache.revision < update.revision -> update
else -> null
}
}
.mapNotNull { it }
}
val lines: List<AdventureLineDbo> = stories.flatMap { story ->
fetchAdventureLine(
documentId = story.documentId,
storyTitle = story.title,
).mapIndexed { index, line ->
adventureDboFactory.convertToDbo(
adventureLine = line,
index = index,
documentId = story.documentId,
storyTitle = story.title,
)
}
} }
database.update( database.update(
booksToRemove = bookToRemove booksToRemove = booksToRemove,
.filter { it.value } storiesToRemove = storiesToRemove,
.map { AdventureDao.BookPartialId(it.key) }, linesToRemove = lines.groupBy { it.id }.keys.toList(),
storiesToRemove = storyToRemove books = books,
.filter { it.value } stories = stories,
.map { AdventureDao.StoryPartialId(it.key.first, it.key.second) }, lines = lines,
adventure = adventures,
) )
} }
@Throws(IncompatibleSheetStructure::class, Exception::class) @Throws(IncompatibleSheetStructure::class, Exception::class)
suspend fun fetchAdventures( suspend fun fetchStories(
bookTitle: String,
documentId: String, documentId: String,
) { ) {
val database = database.adventureDao() val database = database.adventureDao()
val bookStore = database.findAdventures(bookTitle = bookTitle)
val storyToRemove: MutableMap<Pair<String, String>, Boolean> = bookStore.associate { val storiesToRemove = database.findStories(documentId = documentId)
(it.bookTitle to it.adventureTitle) to true .map { it.id }
}.toMutableMap() .toMutableList()
val adventures = fetchAdventuresContent( val stories: List<AdventureStoryDbo> = fetchAdventureStory(documentId = documentId)
database = database, .mapIndexed { index, story ->
storyToRemove = storyToRemove, // convert to StoryBdo
book = AdventureBook( val update = adventureDboFactory.convertToDbo(
title = bookTitle, story = story,
documentId = documentId, index = index,
icon = null, documentId = documentId,
), ).also {
) // Flag this story to not delete it
storiesToRemove.remove(element = it.id)
}
val cache = database.findStory(
documentId = documentId,
title = story.title,
)
when {
cache == null -> update
cache.revision < update.revision -> update
else -> null
}
}
.mapNotNull { it }
val lines: List<AdventureLineDbo> = stories.flatMap { story ->
fetchAdventureLine(
documentId = story.documentId,
storyTitle = story.title,
).mapIndexed { index, line ->
adventureDboFactory.convertToDbo(
adventureLine = line,
index = index,
documentId = story.documentId,
storyTitle = story.title,
)
}
}
database.update( database.update(
booksToRemove = emptyList(), booksToRemove = emptyList(),
storiesToRemove = storyToRemove storiesToRemove = storiesToRemove,
.filter { it.value } linesToRemove = lines.groupBy { it.id }.keys.toList(),
.map { AdventureDao.StoryPartialId(it.key.first, it.key.second) }, books = emptyList(),
adventure = adventures, stories = stories,
lines = lines,
) )
} }
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
@ -157,7 +185,7 @@ class AdventureRepository @Inject constructor(
fun find(bookTitle: String, adventureTitle: String): Adventure? { fun find(bookTitle: String, adventureTitle: String): Adventure? {
return adventure.value.firstOrNull { adventure -> return adventure.value.firstOrNull { adventure ->
adventure.bookTitle == bookTitle && adventure.adventureTitle == adventureTitle adventure.bookTitle == bookTitle && adventure.storyTitle == adventureTitle
} }
} }
@ -179,11 +207,11 @@ class AdventureRepository @Inject constructor(
private suspend fun fetchAdventureLine( private suspend fun fetchAdventureLine(
documentId: String, documentId: String,
adventureTitle: String, storyTitle: String,
): List<AdventureLine> { ): List<AdventureLine> {
return try { return try {
googleRepository.fetch { sheet -> googleRepository.fetch { sheet ->
val request = sheet.get(documentId, adventureTitle) val request = sheet.get(documentId, storyTitle)
adventureStoryLineParser.parse(sheet = request.execute()) adventureStoryLineParser.parse(sheet = request.execute())
} }
} catch (exception: Exception) { } catch (exception: Exception) {

View file

@ -10,7 +10,7 @@ import androidx.navigation.NavType
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.pixelized.rplexicon.ui.navigation.NavigationAnimation import com.pixelized.rplexicon.ui.navigation.NavigationAnimation
import com.pixelized.rplexicon.ui.navigation.animatedComposable import com.pixelized.rplexicon.ui.navigation.animatedComposable
import com.pixelized.rplexicon.ui.screens.adventure.chapter.AdventureChaptersScreen import com.pixelized.rplexicon.ui.screens.adventure.chapter.AdventureStoriesScreen
import com.pixelized.rplexicon.utilitary.extentions.string.ARG import com.pixelized.rplexicon.utilitary.extentions.string.ARG
private const val ROUTE = "adventureDetail" private const val ROUTE = "adventureDetail"
@ -49,7 +49,7 @@ fun NavGraphBuilder.composableAdventureChapters() {
}, },
) )
) { ) {
AdventureChaptersScreen() AdventureStoriesScreen()
} }
} }

View file

@ -52,11 +52,7 @@ class AdventureBooksViewModel @Inject constructor(
init { init {
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.Default) {
try { update()
adventureRepository.fetchAdventures()
} catch (exception: Exception) {
_error.emit(value = Structure(ADVENTURE))
}
} }
} }
@ -66,7 +62,7 @@ class AdventureBooksViewModel @Inject constructor(
_isLoading.value = true _isLoading.value = true
} }
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
adventureRepository.fetchAdventures() adventureRepository.fetchBooks()
} }
} catch (_: Exception) { } catch (_: Exception) {
_error.emit(value = Structure(ADVENTURE)) _error.emit(value = Structure(ADVENTURE))

View file

@ -6,7 +6,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.minimumInteractiveComponentSize import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -23,25 +23,25 @@ import com.pixelized.rplexicon.utilitary.annotateWithDropCap
import com.pixelized.rplexicon.utilitary.extentions.lexicon import com.pixelized.rplexicon.utilitary.extentions.lexicon
@Stable @Stable
sealed class AdventureChapterUio { sealed class AdventureStoriesUio {
@Stable @Stable
data class AdventureCategory( data class AdventureCategory(
val title: String, val title: String,
) : AdventureChapterUio() ) : AdventureStoriesUio()
@Stable @Stable
data class AdventureItem( data class AdventureItem(
val bookTitle: String, val bookTitle: String,
val adventureTitle: String, val adventureTitle: String,
) : AdventureChapterUio() ) : AdventureStoriesUio()
} }
@Composable @Composable
fun AdventureChapterCategory( fun AdventureChapterCategory(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues(horizontal = 16.dp), paddingValues: PaddingValues = PaddingValues(horizontal = 16.dp),
category: AdventureChapterUio.AdventureCategory, category: AdventureStoriesUio.AdventureCategory,
) { ) {
Text( Text(
modifier = modifier.padding(paddingValues = paddingValues), modifier = modifier.padding(paddingValues = paddingValues),
@ -57,7 +57,7 @@ fun AdventureChapterCategory(
fun AdventureChapterItem( fun AdventureChapterItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues(horizontal = 16.dp), paddingValues: PaddingValues = PaddingValues(horizontal = 16.dp),
item: AdventureChapterUio.AdventureItem, item: AdventureStoriesUio.AdventureItem,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
Row( Row(
@ -94,7 +94,7 @@ private fun AdventureChapterCategoryPreview() {
LexiconTheme { LexiconTheme {
Surface { Surface {
AdventureChapterCategory( AdventureChapterCategory(
category = AdventureChapterUio.AdventureCategory( category = AdventureStoriesUio.AdventureCategory(
title = "Les chroniques d'une orc", title = "Les chroniques d'une orc",
), ),
) )
@ -109,7 +109,7 @@ private fun AdventureChapterItemPreview() {
LexiconTheme { LexiconTheme {
Surface { Surface {
AdventureChapterItem( AdventureChapterItem(
item = AdventureChapterUio.AdventureItem( item = AdventureStoriesUio.AdventureItem(
bookTitle = "Les chroniques d'une orc", bookTitle = "Les chroniques d'une orc",
adventureTitle = "La traque", adventureTitle = "La traque",
), ),

View file

@ -46,8 +46,8 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun AdventureChaptersScreen( fun AdventureStoriesScreen(
viewModel: AdventureChaptersViewModel = hiltViewModel(), viewModel: AdventureStoriesViewModel = hiltViewModel(),
) { ) {
val screen = LocalScreenNavHost.current val screen = LocalScreenNavHost.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -89,8 +89,8 @@ private fun AdventureChapterContent(
refreshState: PullRefreshState, refreshState: PullRefreshState,
refreshing: State<Boolean>, refreshing: State<Boolean>,
bookTitle: State<String>, bookTitle: State<String>,
chapters: State<List<AdventureChapterUio>>, chapters: State<List<AdventureStoriesUio>>,
onChapter: (AdventureChapterUio.AdventureItem) -> Unit, onChapter: (AdventureStoriesUio.AdventureItem) -> Unit,
onBack: () -> Unit, onBack: () -> Unit,
) { ) {
Scaffold( Scaffold(
@ -125,7 +125,7 @@ private fun AdventureChapterContent(
) { ) {
itemsIndexed(items = chapters.value) { index, item -> itemsIndexed(items = chapters.value) { index, item ->
when (item) { when (item) {
is AdventureChapterUio.AdventureCategory -> { is AdventureStoriesUio.AdventureCategory -> {
AdventureChapterCategory( AdventureChapterCategory(
modifier = Modifier modifier = Modifier
.padding(top = if (index != 0) 32.dp else 0.dp) .padding(top = if (index != 0) 32.dp else 0.dp)
@ -134,7 +134,7 @@ private fun AdventureChapterContent(
) )
} }
is AdventureChapterUio.AdventureItem -> { is AdventureStoriesUio.AdventureItem -> {
AdventureChapterItem( AdventureChapterItem(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
item = item, item = item,
@ -175,38 +175,38 @@ private fun AdventureChapterPreview() {
chapters = remember { chapters = remember {
mutableStateOf( mutableStateOf(
listOf( listOf(
AdventureChapterUio.AdventureItem( AdventureStoriesUio.AdventureItem(
bookTitle = "Les chroniques d'une orc", bookTitle = "Les chroniques d'une orc",
adventureTitle = "Biographie", adventureTitle = "Biographie",
), ),
AdventureChapterUio.AdventureCategory( AdventureStoriesUio.AdventureCategory(
title = "Mémoire d'une orc", title = "Mémoire d'une orc",
), ),
AdventureChapterUio.AdventureItem( AdventureStoriesUio.AdventureItem(
bookTitle = "Les chroniques d'une orc", bookTitle = "Les chroniques d'une orc",
adventureTitle = "La traque", adventureTitle = "La traque",
), ),
AdventureChapterUio.AdventureItem( AdventureStoriesUio.AdventureItem(
bookTitle = "Les chroniques d'une orc", bookTitle = "Les chroniques d'une orc",
adventureTitle = "Les six mercenaires", adventureTitle = "Les six mercenaires",
), ),
AdventureChapterUio.AdventureItem( AdventureStoriesUio.AdventureItem(
bookTitle = "Les chroniques d'une orc", bookTitle = "Les chroniques d'une orc",
adventureTitle = "La couronne de cuivre", adventureTitle = "La couronne de cuivre",
), ),
AdventureChapterUio.AdventureItem( AdventureStoriesUio.AdventureItem(
bookTitle = "Les chroniques d'une orc", bookTitle = "Les chroniques d'une orc",
adventureTitle = "Le seigneur tout puissant", adventureTitle = "Le seigneur tout puissant",
), ),
AdventureChapterUio.AdventureItem( AdventureStoriesUio.AdventureItem(
bookTitle = "Les chroniques d'une orc", bookTitle = "Les chroniques d'une orc",
adventureTitle = "Vague à l'Amn", adventureTitle = "Vague à l'Amn",
), ),
AdventureChapterUio.AdventureItem( AdventureStoriesUio.AdventureItem(
bookTitle = "Tu parles d'une galère", bookTitle = "Tu parles d'une galère",
adventureTitle = "Tu parles d'une galère", adventureTitle = "Tu parles d'une galère",
), ),
AdventureChapterUio.AdventureItem( AdventureStoriesUio.AdventureItem(
bookTitle = "Les chroniques d'une orc", bookTitle = "Les chroniques d'une orc",
adventureTitle = "Liberté", adventureTitle = "Liberté",
), ),

View file

@ -21,7 +21,7 @@ import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class AdventureChaptersViewModel @Inject constructor( class AdventureStoriesViewModel @Inject constructor(
private val adventureRepository: AdventureRepository, private val adventureRepository: AdventureRepository,
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
) : ViewModel() { ) : ViewModel() {
@ -40,22 +40,22 @@ class AdventureChaptersViewModel @Inject constructor(
.map { adventures -> .map { adventures ->
adventures adventures
.filter { it.bookTitle == argument.bookTitle && it.documentId == argument.documentId } .filter { it.bookTitle == argument.bookTitle && it.documentId == argument.documentId }
.groupBy { it.adventureCategory ?: "" } .groupBy { it.storyCategory ?: "" }
.flatMap { entry -> .flatMap { entry ->
val header = listOf( val header = listOf(
AdventureChapterUio.AdventureCategory(title = entry.key) AdventureStoriesUio.AdventureCategory(title = entry.key)
) )
val stories = entry.value.map { val stories = entry.value.map {
AdventureChapterUio.AdventureItem( AdventureStoriesUio.AdventureItem(
bookTitle = it.bookTitle, bookTitle = it.bookTitle,
adventureTitle = it.adventureTitle, adventureTitle = it.storyTitle,
) )
} }
header + stories header + stories
} }
} }
val chapters: State<List<AdventureChapterUio>> val chapters: State<List<AdventureStoriesUio>>
@Composable @Composable
@Stable @Stable
get() = _chapters.collectAsState(initial = emptyList()) get() = _chapters.collectAsState(initial = emptyList())
@ -66,8 +66,7 @@ class AdventureChaptersViewModel @Inject constructor(
_isLoading.value = true _isLoading.value = true
} }
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
adventureRepository.fetchAdventures( adventureRepository.fetchStories(
bookTitle = argument.bookTitle,
documentId = argument.documentId, documentId = argument.documentId,
) )
} }

View file

@ -52,13 +52,12 @@ import androidx.core.net.toUri
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.images.BackgroundImage import com.pixelized.rplexicon.ui.composable.images.BackgroundImage
import com.pixelized.rplexicon.ui.composable.images.rememberSaturationFilter
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureUio.Style.CHAPTER import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureLineUio.Style.CHAPTER
import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureUio.Style.LEGEND import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureLineUio.Style.LEGEND
import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureUio.Style.PARAGRAPH import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureLineUio.Style.PARAGRAPH
import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureUio.Style.SUB_TITLE import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureLineUio.Style.SUB_TITLE
import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureUio.Style.TITLE import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureLineUio.Style.TITLE
import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.ui.theme.LexiconTheme
@Composable @Composable
@ -101,7 +100,7 @@ private fun AdventureDetailContent(
lazyListState: LazyListState = rememberLazyListState(), lazyListState: LazyListState = rememberLazyListState(),
adventureTitle: State<String?>, adventureTitle: State<String?>,
background: State<Uri?>, background: State<Uri?>,
adventures: State<List<AdventureUio>>, adventures: State<List<AdventureLineUio>>,
onBack: () -> Unit, onBack: () -> Unit,
) { ) {
val nestedScrollOffset = rememberSaveable { mutableFloatStateOf(0f) } val nestedScrollOffset = rememberSaveable { mutableFloatStateOf(0f) }
@ -260,55 +259,55 @@ private fun AdventureDetailPreview() {
adventures = remember { adventures = remember {
mutableStateOf( mutableStateOf(
listOf( listOf(
AdventureUio( AdventureLineUio(
text = "Péripétie", text = "Péripétie",
style = SUB_TITLE, style = SUB_TITLE,
), ),
AdventureUio( AdventureLineUio(
text = "La traque", text = "La traque",
style = TITLE, style = TITLE,
), ),
AdventureUio( AdventureLineUio(
text = "Fake Chapter", text = "Fake Chapter",
style = CHAPTER, style = CHAPTER,
), ),
AdventureUio( AdventureLineUio(
text = "Il était temps pour moi de partir à la chasse. Il ne restait déjà plus grand chose du loup dil y a quelques jours. J'éteignis les dernières braises de mon feu, récupérai dagues, javelot et mon outre deau puis dissimulai le reste de mes biens au fond de la caverne me servant d'abri…", text = "Il était temps pour moi de partir à la chasse. Il ne restait déjà plus grand chose du loup dil y a quelques jours. J'éteignis les dernières braises de mon feu, récupérai dagues, javelot et mon outre deau puis dissimulai le reste de mes biens au fond de la caverne me servant d'abri…",
style = PARAGRAPH, style = PARAGRAPH,
), ),
AdventureUio( AdventureLineUio(
text = "Je me dirigeai vers un point deau où j'avais par le passé déjà aperçu des cervidés. Une fois sur place, je me mis à labri des regards dans un fourré et dissimulai mon odeur avec de la boue. Lattente fut longue, mais la chasse exige patience et discipline. Puis, sur lautre rive, je repérai une harde de cerfs et l'excitation monta en moi.", text = "Je me dirigeai vers un point deau où j'avais par le passé déjà aperçu des cervidés. Une fois sur place, je me mis à labri des regards dans un fourré et dissimulai mon odeur avec de la boue. Lattente fut longue, mais la chasse exige patience et discipline. Puis, sur lautre rive, je repérai une harde de cerfs et l'excitation monta en moi.",
style = PARAGRAPH, style = PARAGRAPH,
), ),
AdventureUio( AdventureLineUio(
text = "Je pris soin de bien choisir ma cible. Tuer un mâle serait gâcher. Seule, je serais incapable de finir sa viande avant quelle ne pourrisse. Je choisis donc une femelle au ventre plat indiquant quelle navait pas mis bas récemment. Dès linstant où ses lèvres touchèrent la surface de la marre, je lançai mon javelot qui frôla la biche pour se planter dans le sol derrière elle. Je bondis de ma cachette pour me précipiter sur ma proie prise de stupeur. Elle ne resta cependant pas figée longtemps. Son instinct de survie prenant le dessus, elle se cabra et s'échappa précédée par ses compagnons.", text = "Je pris soin de bien choisir ma cible. Tuer un mâle serait gâcher. Seule, je serais incapable de finir sa viande avant quelle ne pourrisse. Je choisis donc une femelle au ventre plat indiquant quelle navait pas mis bas récemment. Dès linstant où ses lèvres touchèrent la surface de la marre, je lançai mon javelot qui frôla la biche pour se planter dans le sol derrière elle. Je bondis de ma cachette pour me précipiter sur ma proie prise de stupeur. Elle ne resta cependant pas figée longtemps. Son instinct de survie prenant le dessus, elle se cabra et s'échappa précédée par ses compagnons.",
style = PARAGRAPH, style = PARAGRAPH,
), ),
AdventureUio( AdventureLineUio(
text = "Des taches de sang étaient clairement visibles là où elle se trouvait quelques secondes auparavant. Elle était blessée. Mais pas suffisamment pour être immobilisée. La traque commença. Léchec ou la réussite ne serait plus déterminé que par nos endurances respectives. Tous mes sens en éveil je me lançai sur sa piste. Les branches brisées par une course effrénée danimaux apeurés, des traces de sang expulsées par la contraction de muscles bandés, des empreintes laissées dans le sol meuble dune tourbière ; tous ces indices que seul un œil expérimenté pouvait déceler me permirent de retrouver la harde.", text = "Des taches de sang étaient clairement visibles là où elle se trouvait quelques secondes auparavant. Elle était blessée. Mais pas suffisamment pour être immobilisée. La traque commença. Léchec ou la réussite ne serait plus déterminé que par nos endurances respectives. Tous mes sens en éveil je me lançai sur sa piste. Les branches brisées par une course effrénée danimaux apeurés, des traces de sang expulsées par la contraction de muscles bandés, des empreintes laissées dans le sol meuble dune tourbière ; tous ces indices que seul un œil expérimenté pouvait déceler me permirent de retrouver la harde.",
style = PARAGRAPH, style = PARAGRAPH,
), ),
AdventureUio( AdventureLineUio(
text = "Les effluves de mon corps suant sous leffort me firent repérer, me privant de l'opportunité de tirer à nouveau. Je pus cependant apercevoir ma proie parmi ses congénères. La pauvre bête boitait, larrière cuisse bien entamée par mon arme. Ce marathon arriverait tôt ou tard à son terme et jétais désormais certaine que cette biche assoiffée courait à sa perte. Je continuai donc la traque sans relâche, ce manège se répétant plusieurs fois. Les cerfs plaçant toujours la future victime au centre de la harde, ils mempêchait de latteindre.", text = "Les effluves de mon corps suant sous leffort me firent repérer, me privant de l'opportunité de tirer à nouveau. Je pus cependant apercevoir ma proie parmi ses congénères. La pauvre bête boitait, larrière cuisse bien entamée par mon arme. Ce marathon arriverait tôt ou tard à son terme et jétais désormais certaine que cette biche assoiffée courait à sa perte. Je continuai donc la traque sans relâche, ce manège se répétant plusieurs fois. Les cerfs plaçant toujours la future victime au centre de la harde, ils mempêchait de latteindre.",
style = PARAGRAPH, style = PARAGRAPH,
), ),
AdventureUio( AdventureLineUio(
text = "Mais même lorganisme le plus endurci ne pouvait continuer indéfiniment cette fuite désespérée tout en se vidant de son sang. Je trouvai bientôt ma proie couchée sur le flanc, exténuée, la respiration saccadée, un pas de plus lui aurait coûté la vie. Ses compagnons lavaient abandonnée lorsquelle s'était effondrée, comprenant quil ny avait plus rien à faire pour elle. La fin de sa course signifiait aussi la fin de mon plaisir. La mise à mort dune créature paniquée nétant pas quelque chose que japprécie, je la tuai rapidement dun coup de dague sûr en plein cœur, métant ainsi un terme à ses souffrances.", text = "Mais même lorganisme le plus endurci ne pouvait continuer indéfiniment cette fuite désespérée tout en se vidant de son sang. Je trouvai bientôt ma proie couchée sur le flanc, exténuée, la respiration saccadée, un pas de plus lui aurait coûté la vie. Ses compagnons lavaient abandonnée lorsquelle s'était effondrée, comprenant quil ny avait plus rien à faire pour elle. La fin de sa course signifiait aussi la fin de mon plaisir. La mise à mort dune créature paniquée nétant pas quelque chose que japprécie, je la tuai rapidement dun coup de dague sûr en plein cœur, métant ainsi un terme à ses souffrances.",
style = PARAGRAPH, style = PARAGRAPH,
), ),
AdventureUio( AdventureLineUio(
text = "Bien quexténuée par cette épreuve jétais heureuse. Sa force ferait bientôt partie de moi. Après la mort de lanimal, je pris soin de l'égorger avant de la prendre sur mes épaules. Je sentis son sang encore chaud ruisseler le long de mon dos. Il quittait son corps lentement, comme si la vie rechignait à abandonner cette chair déjà saisie par la rigidité. Cest ainsi chargée du fruit de ma chasse que sur le chemin du retour je sentis lodeur de la chair calcinée.", text = "Bien quexténuée par cette épreuve jétais heureuse. Sa force ferait bientôt partie de moi. Après la mort de lanimal, je pris soin de l'égorger avant de la prendre sur mes épaules. Je sentis son sang encore chaud ruisseler le long de mon dos. Il quittait son corps lentement, comme si la vie rechignait à abandonner cette chair déjà saisie par la rigidité. Cest ainsi chargée du fruit de ma chasse que sur le chemin du retour je sentis lodeur de la chair calcinée.",
style = PARAGRAPH, style = PARAGRAPH,
), ),
AdventureUio( AdventureLineUio(
text = "Je me dirigeai lentement vers cette puanteur, prenant garde de ne faire aucun bruit, jusqu'à ce que jentendis les bribes dune conversation. Consciente de mes limites et de crainte dêtre repérée, je napprochai pas suffisamment pour les voir. Ils s'exprimaient en commun. Lun deux avait un fort accent nain. Un autre était indubitablement dascendance elfique et le dernier était humain. Ils semblaient être en désaccord quant à la direction à prendre. Ne souhaitant pas avoir maille à partir après une journée de course, je pris la décision de les contourner. Les cadavres encore fumants dune troupe orc confirmèrent que javais fait le bon choix. Ces vagabonds n'étaient pas de ceux avec qui il fallait jouer.", text = "Je me dirigeai lentement vers cette puanteur, prenant garde de ne faire aucun bruit, jusqu'à ce que jentendis les bribes dune conversation. Consciente de mes limites et de crainte dêtre repérée, je napprochai pas suffisamment pour les voir. Ils s'exprimaient en commun. Lun deux avait un fort accent nain. Un autre était indubitablement dascendance elfique et le dernier était humain. Ils semblaient être en désaccord quant à la direction à prendre. Ne souhaitant pas avoir maille à partir après une journée de course, je pris la décision de les contourner. Les cadavres encore fumants dune troupe orc confirmèrent que javais fait le bon choix. Ces vagabonds n'étaient pas de ceux avec qui il fallait jouer.",
style = PARAGRAPH, style = PARAGRAPH,
), ),
AdventureUio( AdventureLineUio(
text = "Une fois de retour à mon refuge, je pris le temps de préparer lanimal, coupant les morceaux de viande pour faciliter leur cuisson et leur transport. Javais fait mon choix. Je ne resterais pas encore longtemps dans cette région où la guerre se frayait déjà un chemin. Je m'enfoncerais plus loin dans les terres, méloignant toujours un peu plus de la civilisation.", text = "Une fois de retour à mon refuge, je pris le temps de préparer lanimal, coupant les morceaux de viande pour faciliter leur cuisson et leur transport. Javais fait mon choix. Je ne resterais pas encore longtemps dans cette région où la guerre se frayait déjà un chemin. Je m'enfoncerais plus loin dans les terres, méloignant toujours un peu plus de la civilisation.",
style = PARAGRAPH, style = PARAGRAPH,
), ),
AdventureUio( AdventureLineUio(
text = "Les chroniques de Brulkhaï - 16 ans", text = "Les chroniques de Brulkhaï - 16 ans",
style = LEGEND, style = LEGEND,
), ),

View file

@ -7,7 +7,7 @@ import androidx.lifecycle.ViewModel
import com.pixelized.rplexicon.data.model.adventure.AdventureLine.Format import com.pixelized.rplexicon.data.model.adventure.AdventureLine.Format
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.AdventureUio.Style import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureLineUio.Style
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
@ -25,11 +25,11 @@ class AdventureDetailViewModel @Inject constructor(
) )
val background = derivedStateOf { val background = derivedStateOf {
_detail.value?.adventureBackground _detail.value?.storyBackground
} }
val adventure = derivedStateOf { val adventure = derivedStateOf {
_detail.value?.story?.map { line -> _detail.value?.story?.map { line ->
AdventureUio( AdventureLineUio(
text = line.text, text = line.text,
style = when (line.format) { style = when (line.format) {
Format.TITLE -> Style.TITLE Format.TITLE -> Style.TITLE

View file

@ -10,19 +10,19 @@ import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureUio.Style.ANNEX import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureLineUio.Style.ANNEX
import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureUio.Style.CHAPTER import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureLineUio.Style.CHAPTER
import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureUio.Style.DIALOGUE import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureLineUio.Style.DIALOGUE
import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureUio.Style.LEGEND import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureLineUio.Style.LEGEND
import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureUio.Style.PARAGRAPH import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureLineUio.Style.PARAGRAPH
import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureUio.Style.SUB_TITLE import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureLineUio.Style.SUB_TITLE
import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureUio.Style.TITLE import com.pixelized.rplexicon.ui.screens.adventure.detail.AdventureLineUio.Style.TITLE
import com.pixelized.rplexicon.utilitary.annotateMajWithDropCap import com.pixelized.rplexicon.utilitary.annotateMajWithDropCap
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
@Stable @Stable
data class AdventureUio( data class AdventureLineUio(
val text: String, val text: String,
val style: Style, val style: Style,
) { ) {
@ -41,7 +41,7 @@ data class AdventureUio(
fun AdventureLine( fun AdventureLine(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
paddingValues: PaddingValues, paddingValues: PaddingValues,
item: AdventureUio, item: AdventureLineUio,
) { ) {
Text( Text(
modifier = Modifier modifier = Modifier
@ -80,8 +80,8 @@ fun AdventureLine(
@Stable @Stable
fun rememberPaddingValues( fun rememberPaddingValues(
current: AdventureUio, current: AdventureLineUio,
previous: AdventureUio?, previous: AdventureLineUio?,
): PaddingValues { ): PaddingValues {
return PaddingValues( return PaddingValues(
top = when (current.style) { top = when (current.style) {