Add images support for quest details & quest detail UI clean up.

This commit is contained in:
Thomas Andres Gomez 2023-11-03 16:34:21 +01:00
parent 99d5076168
commit 82ce19ec4d
14 changed files with 209 additions and 259 deletions

View file

@ -21,5 +21,6 @@ data class QuestEntry(
val groupReward: String?,
val individualReward: String?,
val description: String,
val images: List<Uri>,
val background: Uri?,
)

View file

@ -37,7 +37,6 @@ class QuestRepository @Inject constructor(
private suspend fun updateData(data: ValueRange) {
val questEntries = questParser.parse(value = data)
val questMap = questEntries.groupBy { it.title }
val quests = questMap.keys.mapIndexed { index, item ->
Quest(
id = index,

View file

@ -3,79 +3,66 @@ package com.pixelized.rplexicon.repository.parser
import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.model.QuestEntry
import com.pixelized.rplexicon.utilitary.extentions.local.checkSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.sheet
import com.pixelized.rplexicon.utilitary.extentions.toUriOrNull
import javax.inject.Inject
class QuestParser @Inject constructor() {
fun parse(value: ValueRange): List<QuestEntry> {
val sheet = value.values.sheet()
lateinit var structure: Map<String, Int>
return sheet?.mapIndexedNotNull { index, item ->
when {
index == 0 -> {
structure = item.checkSheetStructure(model = COLUMNS)
null
}
item is List<*> -> {
val title = item.getOrNull(structure.title) as? String
val subtitle = item.getOrNull(structure.subtitle) as? String?
val complete = item.getOrNull(structure.complete) as? String?
val questGiver = item.getOrNull(structure.questGiver) as? String?
val area = item.getOrNull(structure.area) as? String?
val groupReward = item.getOrNull(structure.groupReward) as? String?
val individualReward = item.getOrNull(structure.individualReward) as? String?
val description = item.getOrNull(structure.description) as? String
val background = item.getOrNull(structure.background) as? String?
class QuestParser @Inject constructor(
private val imageParser: PortraitParser
) {
fun parse(value: ValueRange): List<QuestEntry> = parserScope {
val quest = mutableListOf<QuestEntry>()
value.forEachRow { index, item ->
when (index) {
0 -> updateStructure(row = item, columns = COLUMNS)
else -> {
val title = item.parse(TITLE)
val description = item.parse(DESCRIPTION)
if (title?.isNotEmpty() == true && description?.isNotEmpty() == true) {
QuestEntry(
val entry = QuestEntry(
sheetIndex = index,
title = title,
subtitle = subtitle?.takeIf { it.isNotBlank() },
complete = complete.equals("TRUE", ignoreCase = true),
questGiver = questGiver?.takeIf { it.isNotBlank() },
area = area?.takeIf { it.isNotBlank() },
groupReward = groupReward?.takeIf { it.isNotBlank() },
individualReward = individualReward?.takeIf { it.isNotBlank() },
subtitle = item.parse(SUB_TITLE),
complete = item.parseBool(COMPLETED) ?: false,
questGiver = item.parse(QUEST_GIVER),
area = item.parse(AREA),
groupReward = item.parse(GROUP_REWARD),
individualReward = item.parse(INDIVIDUAL_REWARD),
description = description,
background = background?.toUriOrNull(),
images = imageParser.parse(item.parse(IMAGE)),
background = item.parseUri(BACKGROUND),
)
} else {
null
quest.add(entry)
}
}
else -> null
}
} ?: emptyList()
}
quest
}
private val Map<String, Int>.title: Int get() = getValue(COLUMNS[0])
private val Map<String, Int>.subtitle: Int get() = getValue(COLUMNS[1])
private val Map<String, Int>.complete: Int get() = getValue(COLUMNS[2])
private val Map<String, Int>.questGiver: Int get() = getValue(COLUMNS[3])
private val Map<String, Int>.area: Int get() = getValue(COLUMNS[4])
private val Map<String, Int>.groupReward: Int get() = getValue(COLUMNS[5])
private val Map<String, Int>.individualReward: Int get() = getValue(COLUMNS[6])
private val Map<String, Int>.description: Int get() = getValue(COLUMNS[7])
private val Map<String, Int>.background: Int get() = getValue(COLUMNS[8])
companion object {
private const val TITLE = "Titre"
private const val SUB_TITLE = "Sous Titre"
private const val COMPLETED = "Compléter"
private const val QUEST_GIVER = "Commanditaire"
private const val AREA = "Lieu"
private const val GROUP_REWARD = "Récompense de groupe"
private const val INDIVIDUAL_REWARD = "Récompense individuelle"
private const val DESCRIPTION = "Description"
private const val IMAGE = "Image"
private const val BACKGROUND = "fond" // TODO
private val COLUMNS = listOf(
"Titre",
"Sous Titre",
"Compléter",
"Commanditaire",
"Lieu",
"Récompense de groupe",
"Récompense individuelle",
"Description",
"fond"
TITLE,
SUB_TITLE,
COMPLETED,
QUEST_GIVER,
AREA,
GROUP_REWARD,
INDIVIDUAL_REWARD,
DESCRIPTION,
IMAGE,
BACKGROUND,
)
}
}

View file

@ -1,5 +1,7 @@
package com.pixelized.rplexicon.repository.parser
import android.net.Uri
import androidx.core.net.toUri
import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.utilitary.extentions.local.checkSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.sheet
@ -34,4 +36,10 @@ class SheetParserScope<T> {
fun List<*>.parseInt(column: String): Int? =
parse(column)?.toIntOrNull()
fun List<*>.parseBool(column: String): Boolean? =
parse(column)?.equals("TRUE", ignoreCase = true)
fun List<*>.parseUri(column: String): Uri? =
parse(column)?.takeIf { it.isNotBlank() }?.toUri()
}

View file

@ -368,16 +368,6 @@ private fun LocationContent(
style = MaterialTheme.typography.headlineSmall,
text = item.value.name,
)
Image(
modifier = Modifier
.height(24.dp)
.graphicsLayer { rotationZ = 180f },
painter = painterResource(id = R.drawable.art_divider_1),
contentScale = ContentScale.FillWidth,
alignment = Alignment.Center,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface),
contentDescription = null,
)
}
HorizontalPager(

View file

@ -2,18 +2,19 @@ package com.pixelized.rplexicon.ui.screens.quest.detail
import android.content.res.Configuration
import android.net.Uri
import androidx.compose.foundation.Image
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
@ -28,18 +29,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@ -49,16 +46,19 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.composable.AsyncImage
import com.pixelized.rplexicon.ui.composable.BackgroundImage
import com.pixelized.rplexicon.ui.composable.FullScreenImageHandler
import com.pixelized.rplexicon.ui.composable.FullScreenImageViewModel
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.navigation.screens.navigateToLexiconDetail
import com.pixelized.rplexicon.ui.navigation.screens.navigateToLocationDetail
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.LOS_FULL
import com.pixelized.rplexicon.utilitary.LOS_HOLLOW
import com.pixelized.rplexicon.utilitary.extentions.annotateWithDropCap
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import com.pixelized.rplexicon.utilitary.extentions.scrollOffset
import java.lang.Integer.min
@Stable
data class QuestDetailUio(
@ -70,82 +70,22 @@ data class QuestDetailUio(
) {
@Stable
data class QuestStep(
val subtitle: String?,
val subtitle: String? = null,
val giverId: Int? = null,
val giver: String?,
val giver: String? = null,
val placeId: Int? = null,
val place: String?,
val globalReward: String?,
val individualReward: String?,
val place: String? = null,
val globalReward: String? = null,
val individualReward: String? = null,
val images: List<Uri> = emptyList(),
val description: String,
)
}
@Stable
data class AnnotatedQuestDetailUio(
val title: String,
val completed: Boolean,
val background: Uri?,
val steps: List<AnnotatedQuestStep>,
) {
@Stable
data class AnnotatedQuestStep(
val subtitle: String?,
val giverId: Int?,
val giver: String?,
val placeId: Int?,
val place: String?,
val individualReward: String?,
val globalReward: String?,
val description: AnnotatedString,
)
}
@Composable
@Stable
private fun QuestDetailUio.annotate(): AnnotatedQuestDetailUio {
val annotatedSteps = steps.map { it.annotate() }
return remember {
AnnotatedQuestDetailUio(
completed = completed,
title = title,
background = background,
steps = annotatedSteps,
)
}
}
@Composable
@Stable
private fun QuestDetailUio.QuestStep.annotate(): AnnotatedQuestDetailUio.AnnotatedQuestStep {
val typography = MaterialTheme.lexicon.typography
return remember {
AnnotatedQuestDetailUio.AnnotatedQuestStep(
subtitle = subtitle,
giverId = giverId,
giver = giver,
placeId = placeId,
place = place,
globalReward = globalReward,
individualReward = individualReward,
description = AnnotatedString(
text = description,
spanStyles = listOf(
AnnotatedString.Range(
item = typography.bodyDropCapSpan,
start = 0,
end = min(1, description.length),
)
)
),
)
}
}
@Composable
fun QuestDetailScreen(
viewModel: QuestDetailViewModel = hiltViewModel(),
imageViewModel: FullScreenImageViewModel = hiltViewModel(),
) {
val screen = LocalScreenNavHost.current
@ -156,6 +96,11 @@ fun QuestDetailScreen(
onBack = { screen.popBackStack() },
onGiver = { screen.navigateToLexiconDetail(id = it) },
onLocation = { screen.navigateToLocationDetail(id = it) },
onImage = { imageViewModel.showDetail(it) }
)
FullScreenImageHandler(
viewModel = imageViewModel,
)
}
}
@ -169,8 +114,9 @@ private fun QuestDetailContent(
onBack: () -> Unit,
onGiver: (Int) -> Unit,
onLocation: (Int) -> Unit,
onImage: (Uri) -> Unit,
) {
val annotatedQuest = item.value.annotate()
val quest = item.value
Scaffold(
modifier = modifier,
@ -203,9 +149,9 @@ private fun QuestDetailContent(
) {
BackgroundImage(
modifier = Modifier.matchParentSize(),
model = annotatedQuest.background,
model = quest.background,
)
if (annotatedQuest.completed) {
if (quest.completed) {
Text(
modifier = Modifier
.align(Alignment.TopEnd)
@ -213,7 +159,7 @@ private fun QuestDetailContent(
.rotate(degrees = 12f),
style = MaterialTheme.lexicon.typography.stamp,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.35f),
text = "Completed",
text = stringResource(id = R.string.quest_detail_completed),
)
}
}
@ -221,80 +167,55 @@ private fun QuestDetailContent(
modifier = Modifier
.verticalScroll(state)
.padding(
top = when (annotatedQuest.background) {
null -> 16.dp
top = when {
quest.background == null && quest.completed -> 96.dp
quest.background == null -> 16.dp
else -> MaterialTheme.lexicon.dimens.detailPadding
},
end = 16.dp,
bottom = 16.dp,
start = 16.dp,
),
verticalArrangement = Arrangement.spacedBy(space = 16.dp),
) {
Column {
Text(
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.displaySmall,
text = annotatedQuest.title,
)
Image(
modifier = Modifier
.height(24.dp)
.graphicsLayer { rotationZ = 180f }
.align(Alignment.CenterHorizontally),
painter = painterResource(id = R.drawable.art_divider_1),
contentScale = ContentScale.FillWidth,
alignment = Alignment.Center,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface),
contentDescription = null,
)
}
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = 16.dp),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineLarge,
text = quest.title.annotateWithDropCap(
style = MaterialTheme.lexicon.typography.headlineLargeDropCap,
),
)
annotatedQuest.steps.forEach { quest ->
quest.steps.forEach { quest ->
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
quest.subtitle?.let { subtitle ->
Row(
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = 64.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
Image(
modifier = Modifier.graphicsLayer { rotationY = 180f },
painter = painterResource(id = R.drawable.art_clip_1),
contentScale = ContentScale.FillWidth,
alignment = Alignment.Center,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface),
contentDescription = null,
)
Text(
modifier = Modifier
.weight(weight = 1f, fill = false)
.padding(horizontal = 8.dp),
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
maxLines = 3,
text = subtitle,
)
Image(
painter = painterResource(id = R.drawable.art_clip_1),
contentScale = ContentScale.FillWidth,
alignment = Alignment.Center,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface),
contentDescription = null,
)
}
.padding(horizontal = 16.dp)
.padding(top = 16.dp),
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
maxLines = 3,
text = subtitle.annotateWithDropCap(
style = MaterialTheme.lexicon.typography.titleLargeDropCap,
),
)
}
quest.giver?.let {
Column(
modifier = Modifier.clickable(
enabled = quest.giverId != null,
onClick = { quest.giverId?.let { onGiver(it) } }
)
modifier = Modifier
.fillMaxWidth()
.clickable(
enabled = quest.giverId != null,
onClick = { quest.giverId?.let { onGiver(it) } }
)
.padding(horizontal = 16.dp)
) {
Text(
style = MaterialTheme.typography.titleMedium,
@ -310,12 +231,16 @@ private fun QuestDetailContent(
)
}
}
quest.place?.let {
Column(
modifier = Modifier.clickable(
enabled = quest.placeId != null,
onClick = { quest.placeId?.let { onLocation(it) } }
)
modifier = Modifier
.fillMaxWidth()
.clickable(
enabled = quest.placeId != null,
onClick = { quest.placeId?.let { onLocation(it) } }
)
.padding(horizontal = 16.dp),
) {
Text(
style = MaterialTheme.typography.titleMedium,
@ -331,8 +256,11 @@ private fun QuestDetailContent(
)
}
}
quest.globalReward?.let {
Column {
Column(
modifier = Modifier.padding(horizontal = 16.dp),
) {
Text(
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
@ -340,12 +268,15 @@ private fun QuestDetailContent(
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = it,
text = "$LOS_HOLLOW $it",
)
}
}
quest.individualReward?.let {
Column {
Column(
modifier = Modifier.padding(horizontal = 16.dp),
) {
Text(
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
@ -353,15 +284,35 @@ private fun QuestDetailContent(
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = it,
text = "$LOS_HOLLOW $it",
)
}
}
Text(
modifier = Modifier.padding(horizontal = 16.dp),
style = MaterialTheme.typography.bodyMedium,
text = quest.description,
text = quest.description.annotateWithDropCap(
style = MaterialTheme.lexicon.typography.bodyMediumDropCap,
),
)
if (quest.images.isNotEmpty()) {
LazyRow(
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
items(items = quest.images) {
AsyncImage(
modifier = Modifier
.clickable { onImage(it) }
.height(height = 160.dp),
contentScale = ContentScale.FillHeight,
model = it,
)
}
}
}
}
}
}
@ -383,6 +334,7 @@ private fun QuestDetailPreview(
onBack = { },
onGiver = { },
onLocation = { },
onImage = { },
)
}
}
@ -402,15 +354,11 @@ private class QuestDetailPreviewProvider : PreviewParameterProvider<State<QuestD
giver = "Sergent d'arme",
place = "DaggerFord",
individualReward = "5po",
globalReward = null,
description = "Des nobles participant aux festivités de DaggerFord aurait entendu des loups dans la forêt proche. Sur ordre du baron, cette forêt doit être fouillée bien que depuis 300 ans, aucun loup n'y ait été vu.",
),
QuestDetailUio.QuestStep(
subtitle = "Partie 2",
giver = "Sergent d'arme",
place = "DaggerFord",
individualReward = "5po. Bonus de 1po par loup.",
globalReward = null,
description = "Nous devons rapporter la dépouille d'un loup pour prouver au sergent que nous avons tué ces bêtes.",
),
)
@ -424,11 +372,7 @@ private class QuestDetailPreviewProvider : PreviewParameterProvider<State<QuestD
title = "Les enfants de la caravanes",
steps = listOf(
QuestDetailUio.QuestStep(
subtitle = null,
giver = null,
place = null,
individualReward = "Pouvoir se regarder dans une glace",
globalReward = null,
globalReward = "Pouvoir se regarder dans une glace",
description = "Une meute de lycan a massacré une caravane marchande quittant DaggerFord. Leur dessin n'est pas encore clair, mais ils ont enlevé les enfants. Nous les pourchassons afin de les sauver.",
)
)

View file

@ -40,6 +40,7 @@ class QuestDetailViewModel @Inject constructor(
globalReward = entry.groupReward,
individualReward = entry.individualReward,
description = entry.description,
images = entry.images,
)
},
)

View file

@ -9,9 +9,11 @@ import com.pixelized.rplexicon.repository.data.lexicon.QuestRepository
import com.pixelized.rplexicon.ui.composable.error.FetchErrorUio
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@HiltViewModel
@ -30,25 +32,34 @@ class QuestListViewModel @Inject constructor(
init {
viewModelScope.launch {
launch {
launch(Dispatchers.IO) {
repository.data.collect { items ->
_items.value = items.map { item ->
QuestItemUio(
id = item.id,
title = item.title,
complete = item.entries.all { it.complete },
)
}.sortedBy { it.title }
val quest = items
.map { item ->
QuestItemUio(
id = item.id,
title = item.title,
complete = item.entries.all { it.complete },
)
}
.sortedBy { it.title }
.sortedBy { it.complete }
withContext(Dispatchers.Main) {
_items.value = quest
}
}
}
launch {
launch(Dispatchers.IO) {
update(force = false)
}
}
}
suspend fun update(force: Boolean) {
_isLoading.value = true
withContext(context = Dispatchers.Main) {
_isLoading.value = true
}
try {
if (force || repository.lastSuccessFullUpdate.shouldUpdate()) {
repository.fetchQuests()
@ -64,9 +75,11 @@ class QuestListViewModel @Inject constructor(
Log.e(TAG, exception.message, exception)
_error.emit(FetchErrorUio.Default)
}
// clean the laoding state
// clean the loading state
finally {
_isLoading.value = false
withContext(context = Dispatchers.Main) {
_isLoading.value = false
}
}
}

View file

@ -39,7 +39,7 @@ data class LexiconDimens(
fun lexiconDimen(
density: Density,
itemHeight: Dp = 52.dp,
detailPadding: Dp = 248.dp,
detailPadding: Dp = 320.dp,
itemListPadding: PaddingValues = PaddingValues(
top = 8.dp,
bottom = 8.dp + 16.dp + 56.dp + 16.dp,

View file

@ -28,18 +28,43 @@ class LexiconTypography(
val stamp: TextStyle = base.headlineLarge.copy(
fontFamily = stampFontFamily,
),
@Deprecated("")
val bodyDropCap: TextStyle = base.headlineLarge.copy(
fontFamily = zallFontFamily,
baselineShift = BaselineShift(-0.1f),
letterSpacing = (-3).sp
),
@Deprecated("")
val titleDropCap: TextStyle = base.displayMedium.copy(
fontFamily = zallFontFamily,
baselineShift = BaselineShift(-0.1f),
letterSpacing = (-4).sp
),
@Deprecated("")
val bodyDropCapSpan: SpanStyle = bodyDropCap.toSpanStyle(),
@Deprecated("")
val titleDropCapSpan: SpanStyle = titleDropCap.toSpanStyle(),
val bodyMediumDropCap: SpanStyle = base.bodyMedium.toDropCapSpan(
sizeRatio = 1.8f,
),
val titleLargeDropCap: SpanStyle = base.titleLarge.toDropCapSpan(
sizeRatio = 1.4f,
),
val headlineLargeDropCap: SpanStyle = base.headlineLarge.toDropCapSpan(
sizeRatio = 1.2f,
),
)
private fun TextStyle.toDropCapSpan(
sizeRatio : Float,
): SpanStyle {
return copy(
fontFamily = zallFontFamily,
fontSize = fontSize * sizeRatio,
).toSpanStyle()
}
fun lexiconTypography() = LexiconTypography()

View file

@ -1,10 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="16dp"
android:viewportWidth="1280"
android:viewportHeight="425">
<path
android:fillColor="#000000"
android:pathData="M961.5,1.9c-9.1,3.3 -16.9,7.2 -25,12.6 -23.2,15.4 -34.6,32.5 -44.4,67 -1.7,6.1 -3.8,12.7 -4.6,14.8 -0.8,2 -1.5,6.6 -1.5,10.1 0,12.6 8.9,24.2 24.7,32.3 9.9,5.1 18.6,7.3 29.4,7.3 11.1,-0 16.6,-1.9 21.7,-7.4 3.4,-3.7 4.1,-5.4 5.2,-11.3 0.9,-5.5 0.9,-8 -0.1,-11.2 -2.7,-9.2 -8.7,-10.1 -28.1,-4.3 -17.4,5.3 -22.7,6 -27.7,3.8 -5,-2.3 -7.1,-6.2 -7.8,-14.4 -1.1,-13.4 3.8,-32.3 12.5,-48.2 5.5,-10 20.2,-25.5 29.4,-30.9 17.4,-10.2 35.8,-13.2 58.2,-9.6 47.7,7.7 88.8,46.1 102.8,96 4.1,14.3 4.7,17.4 5.9,30 2.4,25 -1.3,47.7 -10.9,67 -4.8,9.7 -11,18.7 -29.4,42.9 -40.1,52.4 -60.3,73.6 -96,100.4 -29.1,21.8 -49,32.2 -61.8,32.2 -11.3,-0 -24,-3.4 -73.2,-19.6 -60.6,-19.9 -100.3,-34.7 -154.8,-57.7 -29.3,-12.3 -54.7,-24.4 -108,-51.3 -23.9,-12.1 -52,-25.7 -62.5,-30.3 -58.4,-25.8 -108.4,-38.8 -165.8,-43.2 -20.2,-1.5 -70.9,-0.7 -94.7,1.6 -17.2,1.6 -21.2,2.4 -47.5,8.4 -26.1,5.9 -48.3,7.8 -54.5,4.6 -3.6,-1.8 -3.9,-4.2 -0.7,-5.5 51.5,-21.1 65.9,-26.1 94.1,-32.6 50.5,-11.6 88.6,-16.3 137.3,-17.1 78.9,-1.3 144.1,8 230.3,32.9 33.6,9.7 61.3,20.6 115.7,45.6 23.5,10.8 72.9,31.3 103.1,42.8 12,4.5 39.1,12.5 50.7,14.9 13.3,2.7 30.1,3.8 42.5,2.5 6.3,-0.6 17.6,-1.5 25,-2 22.4,-1.7 35.9,-6.1 49.3,-16.3 11.3,-8.6 18.1,-19.1 17.5,-27 -0.8,-11.1 -14.9,-7.2 -36.8,10.1 -11.8,9.3 -20,13.8 -34.5,18.5l-13,4.2 -20,-0.1c-18.1,-0 -21.1,-0.3 -31.5,-2.7 -27.6,-6.5 -54.9,-16.3 -107.1,-38.5 -16.7,-7.2 -38.5,-16.4 -48.4,-20.5 -9.9,-4.2 -22.9,-9.9 -28.9,-12.7 -40.8,-19.1 -70.8,-29.6 -119.1,-41.5 -23,-5.7 -43.3,-9.6 -70.2,-13.5 -39,-5.7 -56,-7.1 -97.4,-7.7 -76.2,-1.2 -122.4,4.5 -194.4,23.7 -30.9,8.3 -33.1,9.1 -63.5,23.5 -27.6,13.1 -32.2,14.5 -50.2,15.4 -13.6,0.7 -15.1,1 -21,3.9 -11.4,5.6 -16.6,12.1 -25.2,31.4 -4.1,9.2 -10.4,15.9 -17.2,18.2 -5.2,1.7 -13.7,8.4 -27.6,21.3 -12.5,11.6 -12.2,11.1 -9.5,16.4 3,5.7 6.3,6.3 12.1,2.1 12.3,-8.8 40.8,-24.2 44.8,-24.2 3.2,-0 5.9,2 7.4,5.5 1.5,3.6 7.6,8.4 14.7,11.5 7.2,3.2 21.3,5.2 36.2,5.4 13.1,0.1 14.3,-0.1 18.9,-2.4 6.1,-3.1 8.4,-6.1 9.2,-12.1 0.5,-4 0.3,-5 -1.5,-6.8 -2.8,-2.8 -4.6,-2.4 -19.7,3.5 -20,7.9 -28,8.8 -34.1,4.1 -1.4,-1.1 -5.2,-3.3 -8.4,-4.8 -14.2,-6.7 -10.4,-15.5 9.1,-21 4,-1.2 15.7,-6 25.9,-10.9 35.7,-16.8 48.2,-21 56.8,-18.9 11.2,2.7 27,11.6 50.3,28.4 4.7,3.4 14.4,9.5 21.5,13.7 23.9,13.8 101.5,52.4 143.5,71.3 22.8,10.3 48.1,21.8 56.1,25.6 28,13.2 71.3,28.2 129.4,44.8 77.6,22.1 133.6,30.3 195.5,28.7 23.7,-0.6 50.9,-3.1 69.4,-6.2 16.7,-2.8 28.1,-6.1 46.1,-13.2 25.4,-9.9 28,-10.6 39.5,-9.9 5.2,0.3 15.6,2 23,3.6 16.8,3.8 23.5,4.7 42.9,6.1 19.9,1.4 62.4,1.4 77.1,0.1 48.2,-4.5 93.9,-17.2 125.4,-34.8 24.4,-13.6 39.2,-25.4 69.6,-55.3 10.6,-10.4 16.4,-27.4 15.8,-45.8 -0.2,-9.1 -0.7,-11.5 -2.8,-15.6 -3.2,-6.3 -8.2,-9.1 -16.1,-9.1 -13.1,-0 -15.3,5.6 -10.3,26.8 3.6,15.2 3.9,28.4 0.6,34.5 -3.1,5.9 -20.7,23.3 -32.2,31.8 -12.4,9.3 -37,24 -54.5,32.5 -21.1,10.4 -39.2,15.8 -67,20.1 -13.8,2.1 -18.3,2.3 -54.5,2.2 -32.9,-0 -43.4,-0.4 -63,-2.2 -32.4,-3.1 -41.8,-4.3 -40,-5.4 53.4,-31.2 78.9,-50.2 105.1,-77.9 23.9,-25.2 38.3,-43.5 52.9,-67 20.7,-33.3 29,-61.3 27.7,-93.4 -2,-48.8 -25.2,-92.7 -65.7,-124.4 -6.3,-5 -22.7,-13.1 -31.5,-15.6 -21.3,-6.1 -55.1,-8.5 -66,-4.6zM351.5,190.8c32,2 64.4,9.2 115,25.4 37.4,12 60,22.2 110,49.8 43.3,23.9 171.4,80.5 193.8,85.5 4,0.9 20.7,6.2 37.2,11.7 29.9,10 52.5,17 74.8,23 6.4,1.7 11.7,3.5 11.7,3.9 0,0.5 -1.7,1.1 -3.7,1.4 -2.1,0.4 -10.2,2.6 -18,5 -34.9,10.8 -64.3,14.6 -113.4,14.5 -30.8,-0 -43.5,-0.6 -76.9,-4.1 -26.7,-2.7 -42,-5.3 -59,-9.9 -80,-21.6 -116,-32.8 -136,-42.3 -6.3,-3 -15.5,-6.7 -20.5,-8.2 -24.7,-7.5 -94.4,-40.7 -186,-88.4 -24.4,-12.8 -34.9,-18.8 -40.2,-23.2 -4,-3.2 -7.9,-5.9 -8.6,-5.9 -2.5,-0 -40.7,-21.7 -40.7,-23.1 0,-0.4 3,-1.2 6.8,-1.9 3.7,-0.6 14.4,-3.1 23.7,-5.5 20.4,-5.3 33.8,-7.4 54.5,-8.5 18.8,-0.9 53.7,-0.6 75.5,0.8zM143.8,202.7c3.5,0.9 5.9,1.9 5.5,2.3 -0.4,0.4 -6,2.4 -12.3,4.5 -6.2,2 -14.5,5.2 -18.2,7.1 -3.7,1.9 -10,4.7 -14,6.3 -4,1.6 -13.6,5.9 -21.3,9.5 -20,9.4 -21.8,8.9 -5.8,-1.6 11.1,-7.3 14.8,-9.5 32.3,-19.8 6.9,-4.1 13.9,-8.2 15.5,-9.2 3.4,-2 7.6,-1.8 18.3,0.9zM96.5,203c-0.4,0.6 -2.7,2.1 -5.3,3.4 -2.6,1.3 -5.6,3.4 -6.7,4.6 -2.4,2.6 -7.2,5.3 -8.7,4.8 -1.2,-0.4 9.9,-9.1 15.4,-12.1 3.6,-1.9 6.3,-2.3 5.3,-0.7z"
android:strokeColor="#00000000" />
</vector>

File diff suppressed because one or more lines are too long

View file

@ -78,6 +78,7 @@
<string name="search_item_tags">Mots clés :</string>
<string name="quest_detail_title">Détails de quête</string>
<string name="quest_detail_completed">Complétée</string>
<string name="quest_detail_giver">Commanditaire :</string>
<string name="quest_detail_area">Lieu :</string>
<string name="quest_detail_individual_reward">Récompense individuelle :</string>

View file

@ -78,6 +78,7 @@
<string name="search_item_tags">Tags:</string>
<string name="quest_detail_title">Quest details</string>
<string name="quest_detail_completed">Completed</string>
<string name="quest_detail_giver">Quest giver:</string>
<string name="quest_detail_area">Area:</string>
<string name="quest_detail_individual_reward">Individual reward:</string>