Add a quest log feature

This commit is contained in:
Thomas Andres Gomez 2023-08-01 16:08:02 +02:00
parent b3d3e3e314
commit d55e782510
58 changed files with 1740 additions and 452 deletions

View file

@ -11,6 +11,7 @@
android:allowBackup="true" android:allowBackup="true"
android:colorMode="wideColorGamut" android:colorMode="wideColorGamut"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Before After
Before After

View file

@ -0,0 +1,23 @@
package com.pixelized.rplexicon.model
import androidx.compose.runtime.Stable
@Stable
data class Quest(
val id: Int,
val title: String,
val entries: List<QuestEntry>,
)
@Stable
data class QuestEntry(
val sheetIndex: Int,
val title: String,
val subtitle: String?,
val complete: Boolean,
val questGiver: String?,
val area: String?,
val groupReward: String?,
val individualReward: String?,
val description: String,
)

View file

@ -9,6 +9,10 @@ import com.google.api.client.json.gson.GsonFactory
import com.google.api.services.sheets.v4.Sheets import com.google.api.services.sheets.v4.Sheets
import com.google.api.services.sheets.v4.model.ValueRange import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.model.Lexicon import com.pixelized.rplexicon.model.Lexicon
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import com.pixelized.rplexicon.utilitary.exceptions.ServiceNotReady
import com.pixelized.rplexicon.utilitary.extentions.checkSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.sheet
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -59,7 +63,7 @@ class LexiconRepository @Inject constructor(
val lexicon: List<Lexicon> = sheet?.mapIndexedNotNull { index, row -> val lexicon: List<Lexicon> = sheet?.mapIndexedNotNull { index, row ->
when { when {
index == 0 -> { index == 0 -> {
sheetStructure = checkSheetStructure(firstRow = row) sheetStructure = row.checkSheetStructure(model = Sheet.COLUMNS)
null null
} }
@ -79,34 +83,6 @@ class LexiconRepository @Inject constructor(
_data.tryEmit(lexicon) _data.tryEmit(lexicon)
} }
@Throws(IncompatibleSheetStructure::class)
private fun checkSheetStructure(firstRow: Any?): HashMap<String, Int> {
// check if the row is a list
if (firstRow !is ArrayList<*>) {
throw IncompatibleSheetStructure("First row is not a List: $firstRow")
}
// parse the first line to find element that we recognize.
val sheetStructure = hashMapOf<String, Int>()
firstRow.forEachIndexed { index, cell ->
if (cell is String && Sheet.COLUMNS.contains(cell)) {
sheetStructure[cell] = index
}
}
// check if we found everything we need.
when {
sheetStructure.size < Sheet.COLUMNS.size -> throw IncompatibleSheetStructure(
message = "Sheet header row does not have enough column: ${firstRow.size}.\nstructure: $firstRow\nheader: $sheetStructure"
)
sheetStructure.size > Sheet.COLUMNS.size -> throw IncompatibleSheetStructure(
message = "Sheet header row does have too mush columns: ${firstRow.size}.\nstructure: $firstRow\nheader: $sheetStructure"
)
}
return sheetStructure
}
private fun parseCharacterRow( private fun parseCharacterRow(
sheetStructure: Map<String, Int>?, sheetStructure: Map<String, Int>?,
id: Int, id: Int,
@ -159,13 +135,6 @@ class LexiconRepository @Inject constructor(
} }
} }
private fun MutableCollection<Any>?.sheet(): List<*>? {
return this?.firstOrNull {
val sheet = it as? ArrayList<*>
sheet != null
} as List<*>?
}
private fun String?.toUriOrNull(): Uri? = try { private fun String?.toUriOrNull(): Uri? = try {
this?.takeIf { it.isNotBlank() }?.toUri() this?.takeIf { it.isNotBlank() }?.toUri()
} catch (_: Exception) { } catch (_: Exception) {
@ -181,10 +150,6 @@ class LexiconRepository @Inject constructor(
private val Map<String, Int>?.history: Int get() = this?.getValue(Sheet.HISTORY) ?: 6 private val Map<String, Int>?.history: Int get() = this?.getValue(Sheet.HISTORY) ?: 6
private val Map<String, Int>?.tags: Int get() = this?.getValue(Sheet.TAGS) ?: 7 private val Map<String, Int>?.tags: Int get() = this?.getValue(Sheet.TAGS) ?: 7
class ServiceNotReady : Exception()
class IncompatibleSheetStructure(message: String?) : Exception(message)
companion object { companion object {
const val TAG = "LexiconRepository" const val TAG = "LexiconRepository"
} }

View file

@ -0,0 +1,156 @@
package com.pixelized.rplexicon.repository
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import com.google.api.client.extensions.android.http.AndroidHttp
import com.google.api.client.json.gson.GsonFactory
import com.google.api.services.sheets.v4.Sheets
import com.google.api.services.sheets.v4.model.ValueRange
import com.pixelized.rplexicon.model.Quest
import com.pixelized.rplexicon.model.QuestEntry
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import com.pixelized.rplexicon.utilitary.exceptions.ServiceNotReady
import com.pixelized.rplexicon.utilitary.extentions.checkSheetStructure
import com.pixelized.rplexicon.utilitary.extentions.sheet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class QuestRepository @Inject constructor(
private val authenticationRepository: AuthenticationRepository,
) {
private val sheetService: Sheets? by derivedStateOf {
when (authenticationRepository.isAuthenticated.value) {
true -> Sheets
.Builder(
AndroidHttp.newCompatibleTransport(),
GsonFactory(),
authenticationRepository.credential,
)
.build()
else -> null
}
}
private val _data = MutableStateFlow<List<Quest>>(emptyList())
val data: StateFlow<List<Quest>> get() = _data
@Throws(ServiceNotReady::class, IncompatibleSheetStructure::class, Exception::class)
suspend fun fetchQuests() {
val service = sheetService
if (service == null) {
throw ServiceNotReady()
} else {
withContext(Dispatchers.IO) {
val request = service.spreadsheets().values().get(Sheet.ID, Sheet.QUEST_JOURNAL)
val data = request.execute()
updateData(data = data)
}
}
}
@Throws(IncompatibleSheetStructure::class)
private fun updateData(data: ValueRange?) {
val sheet = data?.values?.sheet()
var sheetStructure: Map<String, Int>? = null
val questEntries: List<QuestEntry> = sheet?.mapIndexedNotNull { index, row ->
when {
index == 0 -> {
sheetStructure = row.checkSheetStructure(model = Sheet.COLUMNS)
null
}
row is List<*> -> parseQuestRow(
sheetStructure = sheetStructure,
sheetIndex = index,
row = row,
)
else -> null
}
} ?: emptyList()
val questMap = questEntries.groupBy { it.title }
val quests = questMap.keys.mapIndexed { index, item ->
Quest(
id = index,
title = item,
entries = questMap[item] ?: emptyList(),
)
}
_data.tryEmit(quests)
}
private fun parseQuestRow(
sheetStructure: Map<String, Int>?,
sheetIndex: Int,
row: List<*>?,
): QuestEntry? {
val title = row?.getOrNull(sheetStructure.title) as? String
val subtitle = row?.getOrNull(sheetStructure.subtitle) as? String?
val complete = row?.getOrNull(sheetStructure.complete) as? Boolean? ?: false
val questGiver = row?.getOrNull(sheetStructure.questGiver) as? String?
val area = row?.getOrNull(sheetStructure.area) as? String?
val groupReward = row?.getOrNull(sheetStructure.groupReward) as? String?
val individualReward = row?.getOrNull(sheetStructure.individualReward) as? String?
val description = row?.getOrNull(sheetStructure.description) as? String
return if (title?.isNotEmpty() == true && description?.isNotEmpty() == true) {
QuestEntry(
sheetIndex = sheetIndex,
title = title,
subtitle = subtitle?.takeIf { it.isNotBlank() },
complete = complete,
questGiver = questGiver?.takeIf { it.isNotBlank() },
area = area?.takeIf { it.isNotBlank() },
groupReward = groupReward?.takeIf { it.isNotBlank() },
individualReward = individualReward?.takeIf { it.isNotBlank() },
description = description,
)
} else {
null
}
}
private val Map<String, Int>?.title: Int get() = this?.getValue(Sheet.TITLE) ?: 0
private val Map<String, Int>?.subtitle: Int get() = this?.getValue(Sheet.SUBTITLE) ?: 1
private val Map<String, Int>?.complete: Int get() = this?.getValue(Sheet.COMPLETE) ?: 2
private val Map<String, Int>?.questGiver: Int get() = this?.getValue(Sheet.QUEST_GIVER) ?: 3
private val Map<String, Int>?.area: Int get() = this?.getValue(Sheet.AREA) ?: 4
private val Map<String, Int>?.groupReward: Int get() = this?.getValue(Sheet.G_REWARD) ?: 5
private val Map<String, Int>?.individualReward: Int get() = this?.getValue(Sheet.I_REWARD) ?: 6
private val Map<String, Int>?.description: Int get() = this?.getValue(Sheet.DESCRIPTION) ?: 7
private object Sheet {
const val ID = "1sDAay8DjbRYKM39MvEXWs-RuvyxjOFpOfRZLAEWjIUY"
const val QUEST_JOURNAL = "Journal de quêtes"
val COLUMNS = listOf(
"Titre",
"Sous Titre",
"Compléter",
"Commanditaire",
"Lieu",
"Récompense de groupe",
"Récompense individuelle",
"Description",
)
val TITLE = COLUMNS[0]
val SUBTITLE = COLUMNS[1]
val COMPLETE = COLUMNS[2]
val QUEST_GIVER = COLUMNS[3]
val AREA = COLUMNS[4]
val G_REWARD = COLUMNS[5]
val I_REWARD = COLUMNS[6]
val DESCRIPTION = COLUMNS[7]
}
}

View file

@ -0,0 +1,34 @@
package com.pixelized.rplexicon.ui.composable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.PullRefreshState
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun Loader(
modifier: Modifier = Modifier,
refreshState: PullRefreshState,
refreshing: State<Boolean>,
) {
if (refreshing.value) {
LinearProgressIndicator(
modifier = modifier
.fillMaxWidth()
.clip(shape = CircleShape)
)
}
PullRefreshIndicator(
modifier = modifier,
refreshing = false,
state = refreshState,
)
}

View file

@ -0,0 +1,123 @@
package com.pixelized.rplexicon.ui.navigation
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.navigation.pages.LEXICON_ROUTE
import com.pixelized.rplexicon.ui.navigation.pages.composableLexicon
import com.pixelized.rplexicon.ui.navigation.pages.composableQuestList
import com.pixelized.rplexicon.ui.navigation.pages.navigateToLexicon
import com.pixelized.rplexicon.ui.navigation.pages.navigateToQuestList
val LocalPageNavHost = staticCompositionLocalOf<NavHostController> {
error("LocalScreenNavHost not ready")
}
@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun HomeNavHost(
lexiconListState: LazyListState,
navHostController: NavHostController = rememberAnimatedNavController(),
bottomBarItems: List<BottomBarItem> = rememberBottomBarItems(navHostController = navHostController),
startDestination: String = LEXICON_ROUTE
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = stringResource(id = R.string.app_name))
},
)
},
bottomBar = {
BottomAppBar(
containerColor = MaterialTheme.colorScheme.surface,
tonalElevation = 0.dp,
) {
bottomBarItems.forEach {
NavigationBarItem(
selected = it.selected.value,
onClick = it.onClick,
label = {
Text(text = stringResource(id = it.label))
},
icon = {
Icon(
painter = painterResource(id = it.icon),
contentDescription = "",
)
}
)
}
}
},
content = { padding ->
CompositionLocalProvider(
LocalPageNavHost provides navHostController,
) {
AnimatedNavHost(
modifier = Modifier.padding(padding),
navController = navHostController,
startDestination = startDestination,
) {
composableLexicon(lazyListState = lexiconListState)
composableQuestList()
}
}
}
)
}
@Stable
class BottomBarItem(
val selected: State<Boolean>,
val icon: Int,
val label: Int,
val onClick: () -> Unit,
)
@Composable
@Stable
private fun rememberBottomBarItems(
navHostController: NavHostController,
): List<BottomBarItem> {
return remember(navHostController) {
listOf(
BottomBarItem(
selected = mutableStateOf(false),
icon = R.drawable.ic_outline_account_circle_24,
label = R.string.home_lexicon,
onClick = { navHostController.navigateToLexicon(navHostController.pageOption()) }
),
BottomBarItem(
selected = mutableStateOf(false),
icon = R.drawable.ic_outline_map_24,
label = R.string.home_quest_log,
onClick = { navHostController.navigateToQuestList(navHostController.pageOption()) }
),
)
}
}

View file

@ -1,19 +1,22 @@
package com.pixelized.rplexicon.ui.navigation package com.pixelized.rplexicon.ui.navigation
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.runtime.staticCompositionLocalOf
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.NavOptionsBuilder import androidx.navigation.NavOptionsBuilder
import com.google.accompanist.navigation.animation.AnimatedNavHost import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.rememberAnimatedNavController import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import com.pixelized.rplexicon.ui.navigation.screens.AUTHENTICATION_ROUTE import com.pixelized.rplexicon.ui.navigation.screens.AUTHENTICATION_ROUTE
import com.pixelized.rplexicon.ui.navigation.screens.composableAuthentication import com.pixelized.rplexicon.ui.navigation.screens.composableAuthentication
import com.pixelized.rplexicon.ui.navigation.screens.composableCharacterDetail import com.pixelized.rplexicon.ui.navigation.screens.composableHome
import com.pixelized.rplexicon.ui.navigation.screens.composableLexicon import com.pixelized.rplexicon.ui.navigation.screens.composableLexiconDetail
import com.pixelized.rplexicon.ui.navigation.screens.composableSearch import com.pixelized.rplexicon.ui.navigation.screens.composableLexiconSearch
import com.pixelized.rplexicon.ui.navigation.screens.composableQuestDetail
val LocalScreenNavHost = staticCompositionLocalOf<NavHostController> { val LocalScreenNavHost = staticCompositionLocalOf<NavHostController> {
error("LocalScreenNavHost not ready") error("LocalScreenNavHost not ready")
@ -25,7 +28,7 @@ fun ScreenNavHost(
navHostController: NavHostController = rememberAnimatedNavController(), navHostController: NavHostController = rememberAnimatedNavController(),
startDestination: String = AUTHENTICATION_ROUTE, startDestination: String = AUTHENTICATION_ROUTE,
) { ) {
val lexiconListState = rememberLazyListState() val lexiconListState: LazyListState = rememberLazyListState()
CompositionLocalProvider( CompositionLocalProvider(
LocalScreenNavHost provides navHostController, LocalScreenNavHost provides navHostController,
@ -35,9 +38,12 @@ fun ScreenNavHost(
startDestination = startDestination, startDestination = startDestination,
) { ) {
composableAuthentication() composableAuthentication()
composableLexicon(lazyListState = lexiconListState) composableHome(lexiconListState)
composableCharacterDetail()
composableSearch() composableLexiconDetail()
composableLexiconSearch()
composableQuestDetail()
} }
} }
} }
@ -50,3 +56,15 @@ fun rootOption(): NavOptionsBuilder.() -> Unit = {
inclusive = true inclusive = true
} }
} }
fun NavHostController.pageOption(): NavOptionsBuilder.() -> Unit = {
// Pop up to the start destination of the graph to avoid building up a
// large stack of destinations on the back stack as users select items
popUpTo(graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when re-selecting the same item
launchSingleTop = true
// Restore state when re-selecting a previously selected item
restoreState = true
}

View file

@ -1,4 +1,4 @@
package com.pixelized.rplexicon.ui.navigation.screens package com.pixelized.rplexicon.ui.navigation.pages
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
@ -6,7 +6,7 @@ import androidx.navigation.NavHostController
import androidx.navigation.NavOptionsBuilder import androidx.navigation.NavOptionsBuilder
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.lexicon.LexiconScreen import com.pixelized.rplexicon.ui.screens.lexicon.list.LexiconScreen
private const val ROUTE = "lexicon" private const val ROUTE = "lexicon"
@ -17,7 +17,7 @@ fun NavGraphBuilder.composableLexicon(
) { ) {
animatedComposable( animatedComposable(
route = LEXICON_ROUTE, route = LEXICON_ROUTE,
animation = NavigationAnimation.Fade, animation = NavigationAnimation.NONE,
) { ) {
LexiconScreen( LexiconScreen(
lazyListState = lazyListState lazyListState = lazyListState

View file

@ -0,0 +1,27 @@
package com.pixelized.rplexicon.ui.navigation.pages
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.NavOptionsBuilder
import com.pixelized.rplexicon.ui.navigation.NavigationAnimation
import com.pixelized.rplexicon.ui.navigation.animatedComposable
import com.pixelized.rplexicon.ui.screens.quest.list.QuestListScreen
private const val ROUTE = "quests"
const val QUEST_LIST_ROUTE = ROUTE
fun NavGraphBuilder.composableQuestList() {
animatedComposable(
route = QUEST_LIST_ROUTE,
animation = NavigationAnimation.NONE,
) {
QuestListScreen()
}
}
fun NavHostController.navigateToQuestList(
option: NavOptionsBuilder.() -> Unit = {},
) {
navigate(route = ROUTE, builder = option)
}

View file

@ -0,0 +1,30 @@
package com.pixelized.rplexicon.ui.navigation.screens
import androidx.compose.foundation.lazy.LazyListState
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.NavOptionsBuilder
import com.pixelized.rplexicon.ui.navigation.HomeNavHost
import com.pixelized.rplexicon.ui.navigation.NavigationAnimation
import com.pixelized.rplexicon.ui.navigation.animatedComposable
private const val ROUTE = "home"
const val HOME_ROUTE = ROUTE
fun NavGraphBuilder.composableHome(
lexiconListState: LazyListState,
) {
animatedComposable(
route = HOME_ROUTE,
animation = NavigationAnimation.Fade,
) {
HomeNavHost(lexiconListState = lexiconListState)
}
}
fun NavHostController.navigateToHome(
option: NavOptionsBuilder.() -> Unit = {},
) {
navigate(route = ROUTE, builder = option)
}

View file

@ -11,10 +11,10 @@ import androidx.navigation.navArgument
import com.pixelized.rplexicon.model.Lexicon import com.pixelized.rplexicon.model.Lexicon
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.detail.CharacterDetailScreen import com.pixelized.rplexicon.ui.screens.lexicon.detail.LexiconDetailScreen
import com.pixelized.rplexicon.utilitary.extentions.ARG import com.pixelized.rplexicon.utilitary.extentions.ARG
private const val ROUTE = "CharacterDetail" private const val ROUTE = "LexiconDetail"
private const val ARG_ID = "id" private const val ARG_ID = "id"
private const val ARG_HIGHLIGHT = "highlight" private const val ARG_HIGHLIGHT = "highlight"
private const val ARG_RACE = "race" private const val ARG_RACE = "race"
@ -22,16 +22,7 @@ private const val ARG_HIGHLIGHT_RACE = "highlightRace"
private const val ARG_GENDER = "gender" private const val ARG_GENDER = "gender"
private const val ARG_HIGHLIGHT_GENDER = "highlightGender" private const val ARG_HIGHLIGHT_GENDER = "highlightGender"
//CharacterDetail val LEXICON_DETAIL_ROUTE = ROUTE +
// ?id=0
// &highlight=null
// &race=false
// &highlightRace=UNDETERMINED
// &gender=false
// &highlightGender=UNDETERMINED
val CHARACTER_DETAIL_ROUTE = ROUTE +
"?${ARG_ID.ARG}" + "?${ARG_ID.ARG}" +
"&${ARG_HIGHLIGHT.ARG}" + "&${ARG_HIGHLIGHT.ARG}" +
"&${ARG_RACE.ARG}" + "&${ARG_RACE.ARG}" +
@ -41,7 +32,7 @@ val CHARACTER_DETAIL_ROUTE = ROUTE +
@Stable @Stable
@Immutable @Immutable
data class CharacterDetailArgument( data class LexiconDetailArgument(
val id: Int, val id: Int,
val highlight: String?, val highlight: String?,
val race: Lexicon.Race, val race: Lexicon.Race,
@ -50,8 +41,8 @@ data class CharacterDetailArgument(
val highlightGender: Boolean, val highlightGender: Boolean,
) )
val SavedStateHandle.characterDetailArgument: CharacterDetailArgument val SavedStateHandle.lexiconDetailArgument: LexiconDetailArgument
get() = CharacterDetailArgument( get() = LexiconDetailArgument(
id = get(ARG_ID) id = get(ARG_ID)
?: error("CharacterDetailArgument argument: $ARG_ID"), ?: error("CharacterDetailArgument argument: $ARG_ID"),
race = get(ARG_RACE) race = get(ARG_RACE)
@ -65,9 +56,9 @@ val SavedStateHandle.characterDetailArgument: CharacterDetailArgument
highlight = get(ARG_HIGHLIGHT), highlight = get(ARG_HIGHLIGHT),
) )
fun NavGraphBuilder.composableCharacterDetail() { fun NavGraphBuilder.composableLexiconDetail() {
animatedComposable( animatedComposable(
route = CHARACTER_DETAIL_ROUTE, route = LEXICON_DETAIL_ROUTE,
arguments = listOf( arguments = listOf(
navArgument(name = ARG_ID) { navArgument(name = ARG_ID) {
type = NavType.IntType type = NavType.IntType
@ -91,11 +82,11 @@ fun NavGraphBuilder.composableCharacterDetail() {
), ),
animation = NavigationAnimation.Push, animation = NavigationAnimation.Push,
) { ) {
CharacterDetailScreen() LexiconDetailScreen()
} }
} }
fun NavHostController.navigateToCharacterDetail( fun NavHostController.navigateToLexiconDetail(
id: Int, id: Int,
highlight: String? = null, highlight: String? = null,
race: Lexicon.Race? = null, race: Lexicon.Race? = null,

View file

@ -5,13 +5,13 @@ import androidx.navigation.NavHostController
import androidx.navigation.NavOptionsBuilder import androidx.navigation.NavOptionsBuilder
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.search.SearchScreen import com.pixelized.rplexicon.ui.screens.lexicon.search.SearchScreen
private const val ROUTE = "search" private const val ROUTE = "search"
const val SEARCH_ROUTE = ROUTE const val SEARCH_ROUTE = ROUTE
fun NavGraphBuilder.composableSearch() { fun NavGraphBuilder.composableLexiconSearch() {
animatedComposable( animatedComposable(
route = SEARCH_ROUTE, route = SEARCH_ROUTE,
animation = NavigationAnimation.Push, animation = NavigationAnimation.Push,
@ -20,7 +20,7 @@ fun NavGraphBuilder.composableSearch() {
} }
} }
fun NavHostController.navigateToSearch( fun NavHostController.navigateToLexiconSearch(
option: NavOptionsBuilder.() -> Unit = {}, option: NavOptionsBuilder.() -> Unit = {},
) { ) {
navigate(route = ROUTE, builder = option) navigate(route = ROUTE, builder = option)

View file

@ -0,0 +1,53 @@
package com.pixelized.rplexicon.ui.navigation.screens
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.NavOptionsBuilder
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.pixelized.rplexicon.ui.navigation.NavigationAnimation
import com.pixelized.rplexicon.ui.navigation.animatedComposable
import com.pixelized.rplexicon.ui.screens.quest.detail.QuestDetailScreen
import com.pixelized.rplexicon.utilitary.extentions.ARG
private const val ROUTE = "QuestDetail"
private const val ARG_ID = "id"
val QUEST_DETAIL_ROUTE = "$ROUTE?${ARG_ID.ARG}"
@Stable
@Immutable
data class QuestDetailArgument(
val id: Int,
)
val SavedStateHandle.questDetailArgument: QuestDetailArgument
get() = QuestDetailArgument(
id = get(ARG_ID) ?: error("CharacterDetailArgument argument: $ARG_ID"),
)
fun NavGraphBuilder.composableQuestDetail() {
animatedComposable(
route = QUEST_DETAIL_ROUTE,
arguments = listOf(
navArgument(name = ARG_ID) {
type = NavType.IntType
},
),
animation = NavigationAnimation.Push,
) {
QuestDetailScreen()
}
}
fun NavHostController.navigateToQuestDetail(
id: Int,
option: NavOptionsBuilder.() -> Unit = {},
) {
val route = "$ROUTE?$ARG_ID=$id"
navigate(route = route, builder = option)
}

View file

@ -52,7 +52,7 @@ import com.pixelized.rplexicon.LocalActivity
import com.pixelized.rplexicon.R import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.navigation.rootOption import com.pixelized.rplexicon.ui.navigation.rootOption
import com.pixelized.rplexicon.ui.navigation.screens.navigateToLexicon import com.pixelized.rplexicon.ui.navigation.screens.navigateToHome
import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.ui.theme.colors.GoogleColorPalette import com.pixelized.rplexicon.ui.theme.colors.GoogleColorPalette
import com.pixelized.rplexicon.utilitary.sensor.Gyroscope import com.pixelized.rplexicon.utilitary.sensor.Gyroscope
@ -88,7 +88,7 @@ fun AuthenticationScreen(
HandleAuthenticationState( HandleAuthenticationState(
state = state, state = state,
onSignIn = { onSignIn = {
screen.navigateToLexicon(option = rootOption()) screen.navigateToHome(option = rootOption())
}, },
) )
} }

View file

@ -1,132 +0,0 @@
package com.pixelized.rplexicon.ui.screens.lexicon
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.placeholder
@Stable
data class LexiconItemUio(
val id: Int,
val name: String,
val diminutive: String?,
@StringRes val gender: Int,
@StringRes val race: Int,
val placeholder: Boolean = false,
) {
companion object {
val Brulkhai = LexiconItemUio(
id = 0,
name = "Brulkhai",
diminutive = "Bru",
gender = R.string.gender_female_short,
race = R.string.race_half_orc,
placeholder = true,
)
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun LexiconItem(
modifier: Modifier = Modifier,
item: LexiconItemUio,
) {
val typography = MaterialTheme.typography
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
modifier = Modifier
.alignByBaseline()
.placeholder { item.placeholder },
style = remember { typography.bodyLarge.copy(fontWeight = FontWeight.Bold) },
maxLines = 1,
text = item.name,
)
Text(
modifier = Modifier
.alignByBaseline()
.placeholder { item.placeholder },
style = typography.labelMedium,
maxLines = 1,
text = item.diminutive ?: ""
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
modifier = Modifier
.alignByBaseline()
.placeholder { item.placeholder },
style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) },
maxLines = 1,
text = stringResource(id = item.gender)
)
Text(
modifier = Modifier
.alignByBaseline()
.placeholder { item.placeholder },
style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) },
maxLines = 1,
text = stringResource(id = item.race)
)
}
}
}
}
@Composable
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun LexiconItemContentPreview() {
LexiconTheme {
Surface {
LexiconItem(
item = LexiconItemUio(
id = 0,
name = "Brulkhai",
diminutive = "Bru",
gender = R.string.gender_female_short,
race = R.string.race_half_orc,
)
)
}
}
}

View file

@ -1,4 +1,4 @@
package com.pixelized.rplexicon.ui.screens.detail package com.pixelized.rplexicon.ui.screens.lexicon.detail
import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
@ -69,7 +69,7 @@ import com.pixelized.rplexicon.utilitary.extentions.highlightRegex
import com.skydoves.landscapist.ImageOptions import com.skydoves.landscapist.ImageOptions
@Stable @Stable
data class CharacterDetailUio( data class LexiconDetailUio(
val name: String, val name: String,
val diminutive: String?, val diminutive: String?,
val gender: Lexicon.Gender, val gender: Lexicon.Gender,
@ -84,7 +84,7 @@ data class CharacterDetailUio(
) )
@Stable @Stable
data class AnnotatedCharacterDetailUio( data class AnnotatedLexiconDetailUio(
val name: AnnotatedString, val name: AnnotatedString,
val diminutive: AnnotatedString?, val diminutive: AnnotatedString?,
val gender: AnnotatedString, val gender: AnnotatedString,
@ -97,7 +97,7 @@ data class AnnotatedCharacterDetailUio(
@Composable @Composable
@Stable @Stable
fun CharacterDetailUio.annotated(): AnnotatedCharacterDetailUio { fun LexiconDetailUio.annotate(): AnnotatedLexiconDetailUio {
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val highlight = remember { SpanStyle(color = colorScheme.primary) } val highlight = remember { SpanStyle(color = colorScheme.primary) }
val highlightRegex = remember(search) { search.highlightRegex } val highlightRegex = remember(search) { search.highlightRegex }
@ -105,7 +105,7 @@ fun CharacterDetailUio.annotated(): AnnotatedCharacterDetailUio {
val race = stringResource(id = race) val race = stringResource(id = race)
return remember(search, race, highlightRace, gender, highlightGender) { return remember(search, race, highlightRace, gender, highlightGender) {
AnnotatedCharacterDetailUio( AnnotatedLexiconDetailUio(
portrait = portrait, portrait = portrait,
name = AnnotatedString( name = AnnotatedString(
text = name, text = name,
@ -148,13 +148,13 @@ fun CharacterDetailUio.annotated(): AnnotatedCharacterDetailUio {
} }
@Composable @Composable
fun CharacterDetailScreen( fun LexiconDetailScreen(
viewModel: CharacterDetailViewModel = hiltViewModel(), viewModel: LexiconDetailViewModel = hiltViewModel(),
) { ) {
val screen = LocalScreenNavHost.current val screen = LocalScreenNavHost.current
Surface { Surface {
CharacterDetailScreenContent( LexiconDetailContent(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
item = viewModel.character, item = viewModel.character,
onBack = { screen.popBackStack() }, onBack = { screen.popBackStack() },
@ -164,15 +164,15 @@ fun CharacterDetailScreen(
@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) @OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class)
@Composable @Composable
private fun CharacterDetailScreenContent( private fun LexiconDetailContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
state: ScrollState = rememberScrollState(), state: ScrollState = rememberScrollState(),
item: State<CharacterDetailUio>, item: State<LexiconDetailUio>,
onBack: () -> Unit, onBack: () -> Unit,
) { ) {
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val typography = MaterialTheme.typography val typography = MaterialTheme.typography
val annotatedItem = item.value.annotated() val annotatedItem = item.value.annotate()
Scaffold( Scaffold(
modifier = modifier, modifier = modifier,
@ -244,7 +244,8 @@ private fun CharacterDetailScreenContent(
annotatedItem.diminutive?.let { annotatedItem.diminutive?.let {
Text( Text(
modifier = Modifier.alignByBaseline(), modifier = Modifier.alignByBaseline(),
style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) }, style = typography.labelMedium,
fontStyle = FontStyle.Italic,
text = it, text = it,
) )
} }
@ -255,11 +256,13 @@ private fun CharacterDetailScreenContent(
horizontalArrangement = Arrangement.spacedBy(4.dp) horizontalArrangement = Arrangement.spacedBy(4.dp)
) { ) {
Text( Text(
style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) }, style = typography.labelMedium,
fontStyle = FontStyle.Italic,
text = annotatedItem.gender, text = annotatedItem.gender,
) )
Text( Text(
style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) }, style = typography.labelMedium,
fontStyle = FontStyle.Italic,
text = annotatedItem.race, text = annotatedItem.race,
) )
} }
@ -329,7 +332,8 @@ private fun CharacterDetailScreenContent(
annotatedItem.tags?.let { annotatedItem.tags?.let {
Text( Text(
modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp), modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp),
style = remember { typography.labelSmall.copy(fontStyle = FontStyle.Italic) }, style = typography.labelSmall,
fontStyle = FontStyle.Italic,
text = it, text = it,
) )
} }
@ -374,14 +378,14 @@ private fun Modifier.scrollOffset(
} }
@Composable @Composable
@Preview(uiMode = UI_MODE_NIGHT_NO) @Preview(uiMode = UI_MODE_NIGHT_NO, heightDp = 980)
@Preview(uiMode = UI_MODE_NIGHT_YES) @Preview(uiMode = UI_MODE_NIGHT_YES, heightDp = 980)
private fun CharacterDetailScreenContentPreview() { private fun LexiconDetailPreview() {
LexiconTheme { LexiconTheme {
Surface { Surface {
val character = remember { val character = remember {
mutableStateOf( mutableStateOf(
CharacterDetailUio( LexiconDetailUio(
name = "Brulkhai", name = "Brulkhai",
diminutive = "./ Bru", diminutive = "./ Bru",
gender = Lexicon.Gender.FEMALE, gender = Lexicon.Gender.FEMALE,
@ -391,14 +395,14 @@ private fun CharacterDetailScreenContentPreview() {
), ),
description = "Brulkhai, ou plus simplement Bru, est solidement bâti. Elle mesure 192 cm pour 110 kg de muscles lorsquelle est en bonne santé. Elle a les cheveux châtains, les yeux noisettes et la peau couleur gris-vert typique de son espèce. Dun tempérament taciturne, elle parle peu et de façon concise. Elle est parfois brutale, aussi bien physiquement que verbalement, Elle ne prend cependant aucun plaisir à malmener ceux quelle considère plus faibles quelle. Dune nature simple et honnête, elle ne mâche pas ses mots et ne dissimule généralement pas ses pensées. Son intelligence modeste est plus le reflet dun manque déducation et dune capacité limitée à gérer ses émotions quà une débilité congénitale. Elle voue à la force un culte car cest par son expression quelle se sent vraiment vivante et éprouve de grandes difficultés vis à vis de ceux quelle nomme foshnu (bébé, chouineur en commun).", description = "Brulkhai, ou plus simplement Bru, est solidement bâti. Elle mesure 192 cm pour 110 kg de muscles lorsquelle est en bonne santé. Elle a les cheveux châtains, les yeux noisettes et la peau couleur gris-vert typique de son espèce. Dun tempérament taciturne, elle parle peu et de façon concise. Elle est parfois brutale, aussi bien physiquement que verbalement, Elle ne prend cependant aucun plaisir à malmener ceux quelle considère plus faibles quelle. Dune nature simple et honnête, elle ne mâche pas ses mots et ne dissimule généralement pas ses pensées. Son intelligence modeste est plus le reflet dun manque déducation et dune capacité limitée à gérer ses émotions quà une débilité congénitale. Elle voue à la force un culte car cest par son expression quelle se sent vraiment vivante et éprouve de grandes difficultés vis à vis de ceux quelle nomme foshnu (bébé, chouineur en commun).",
history = null, history = null,
tags = null, tags = "protagoniste, brute",
search = "Bru", search = "Bru",
highlightGender = true, highlightGender = true,
highlightRace = true, highlightRace = true,
) )
) )
} }
CharacterDetailScreenContent( LexiconDetailContent(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
item = character, item = character,
onBack = { }, onBack = { },

View file

@ -1,27 +1,27 @@
package com.pixelized.rplexicon.ui.screens.detail package com.pixelized.rplexicon.ui.screens.lexicon.detail
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.pixelized.rplexicon.repository.LexiconRepository import com.pixelized.rplexicon.repository.LexiconRepository
import com.pixelized.rplexicon.ui.navigation.screens.characterDetailArgument import com.pixelized.rplexicon.ui.navigation.screens.lexiconDetailArgument
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class CharacterDetailViewModel @Inject constructor( class LexiconDetailViewModel @Inject constructor(
savedStateHandle: SavedStateHandle, savedStateHandle: SavedStateHandle,
repository: LexiconRepository, repository: LexiconRepository,
) : ViewModel() { ) : ViewModel() {
val character: State<CharacterDetailUio> val character: State<LexiconDetailUio>
init { init {
val argument = savedStateHandle.characterDetailArgument val argument = savedStateHandle.lexiconDetailArgument
val source = repository.data.value[argument.id] val source = repository.data.value[argument.id]
character = mutableStateOf( character = mutableStateOf(
CharacterDetailUio( LexiconDetailUio(
name = source.name, name = source.name,
diminutive = source.diminutive?.let { "./ $it" }, diminutive = source.diminutive?.let { "./ $it" },
gender = source.gender, gender = source.gender,

View file

@ -0,0 +1,182 @@
package com.pixelized.rplexicon.ui.screens.lexicon.list
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
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.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.cell
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import com.pixelized.rplexicon.utilitary.extentions.placeholder
@Stable
data class LexiconItemUio(
val id: Int,
val name: String,
val diminutive: String?,
@StringRes val gender: Int,
@StringRes val race: Int,
val placeholder: Boolean = false,
) {
companion object {
@Stable
fun preview(
id: Int = 0,
name: String = "Brulkhai",
diminutive: String? = null,
@StringRes gender: Int = R.string.gender_female_short,
@StringRes race: Int = R.string.race_half_orc,
placeholder: Boolean = false,
) = LexiconItemUio(
id = id,
name = name,
diminutive = diminutive,
gender = gender,
race = race,
placeholder = placeholder,
)
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun LexiconItem(
modifier: Modifier = Modifier,
item: LexiconItemUio,
) {
val typography = MaterialTheme.lexicon.typography
Box(
modifier = modifier,
contentAlignment = Alignment.CenterStart,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
modifier = Modifier.alignByBaseline(),
text = "",
)
FlowRow(
modifier = Modifier.alignByBaseline(),
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
modifier = Modifier
.alignByBaseline()
.placeholder { item.placeholder },
style = typography.base.bodyLarge,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
text = AnnotatedString(
text = item.name,
spanStyles = when (item.placeholder) {
true -> emptyList()
else -> listOf(
AnnotatedString.Range(
item = typography.dropCapMediumSpan,
start = 0,
end = 1,
)
)
},
),
)
item.diminutive?.let {
Text(
modifier = Modifier
.alignByBaseline()
.placeholder { item.placeholder },
style = typography.base.labelMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
text = it,
)
}
Row(
modifier = Modifier.alignByBaseline(),
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
modifier = Modifier
.alignByBaseline()
.placeholder { item.placeholder },
style = typography.base.labelMedium,
fontStyle = FontStyle.Italic,
maxLines = 1,
text = stringResource(id = item.gender)
)
Text(
modifier = Modifier
.alignByBaseline()
.placeholder { item.placeholder },
style = typography.base.labelMedium,
fontStyle = FontStyle.Italic,
maxLines = 1,
text = stringResource(id = item.race)
)
}
}
}
}
}
@Composable
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun LexiconItemPreview(
@PreviewParameter(LexiconItemPreviewProvider::class) preview: LexiconItemUio
) {
LexiconTheme {
Surface {
LexiconItem(
modifier = Modifier.cell(),
item = preview
)
}
}
}
private class LexiconItemPreviewProvider : PreviewParameterProvider<LexiconItemUio> {
override val values: Sequence<LexiconItemUio> = sequenceOf(
LexiconItemUio.preview(
name = "Mundas-Naltum-Brulkhai-Arauishi",
diminutive = "Mun-Nalt-Bru-Arahi",
placeholder = false,
),
LexiconItemUio.preview(
name = "Brulkhai",
diminutive = "Bru",
placeholder = false,
),
LexiconItemUio.preview(
placeholder = true,
),
)
}

View file

@ -1,4 +1,4 @@
package com.pixelized.rplexicon.ui.screens.lexicon package com.pixelized.rplexicon.ui.screens.lexicon.list
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
@ -16,29 +16,20 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.PullRefreshState import androidx.compose.material.pullrefresh.PullRefreshState
import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
@ -49,7 +40,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -57,17 +47,17 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.LocalSnack import com.pixelized.rplexicon.LocalSnack
import com.pixelized.rplexicon.NO_WINDOW_INSETS
import com.pixelized.rplexicon.R import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.composable.FloatingActionButton import com.pixelized.rplexicon.ui.composable.FloatingActionButton
import com.pixelized.rplexicon.ui.composable.Loader
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.navigation.screens.navigateToCharacterDetail import com.pixelized.rplexicon.ui.navigation.screens.navigateToLexiconDetail
import com.pixelized.rplexicon.ui.navigation.screens.navigateToSearch import com.pixelized.rplexicon.ui.navigation.screens.navigateToLexiconSearch
import com.pixelized.rplexicon.ui.screens.lexicon.LexiconErrorUio.Default import com.pixelized.rplexicon.ui.screens.lexicon.list.LexiconErrorUio.Default
import com.pixelized.rplexicon.ui.screens.lexicon.LexiconErrorUio.Permission import com.pixelized.rplexicon.ui.screens.lexicon.list.LexiconErrorUio.Permission
import com.pixelized.rplexicon.ui.screens.lexicon.LexiconErrorUio.Structure import com.pixelized.rplexicon.ui.screens.lexicon.list.LexiconErrorUio.Structure
import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.lexicon import com.pixelized.rplexicon.utilitary.extentions.cell
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -109,17 +99,16 @@ fun LexiconScreen(
Surface { Surface {
LexiconScreenContent( LexiconScreenContent(
modifier = Modifier.systemBarsPadding(),
items = viewModel.items, items = viewModel.items,
lazyColumnState = lazyListState, lazyColumnState = lazyListState,
refreshState = refresh, refreshState = refresh,
refreshing = viewModel.isLoading, refreshing = viewModel.isLoading,
isFabExpended = isFabExpended, isFabExpended = isFabExpended,
onSearch = { onSearch = {
screen.navigateToSearch() screen.navigateToLexiconSearch()
}, },
onItem = { onItem = {
screen.navigateToCharacterDetail(id = it.id) screen.navigateToLexiconDetail(id = it.id)
}, },
) )
@ -133,7 +122,6 @@ fun LexiconScreen(
} }
@OptIn( @OptIn(
ExperimentalMaterial3Api::class,
ExperimentalMaterialApi::class, ExperimentalMaterialApi::class,
ExperimentalAnimationApi::class, ExperimentalAnimationApi::class,
) )
@ -141,6 +129,10 @@ fun LexiconScreen(
private fun LexiconScreenContent( private fun LexiconScreenContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
lazyColumnState: LazyListState, lazyColumnState: LazyListState,
paddingValues: PaddingValues = PaddingValues(
top = 6.dp,
bottom = 8.dp + 16.dp + 56.dp + 16.dp,
),
refreshState: PullRefreshState, refreshState: PullRefreshState,
refreshing: State<Boolean>, refreshing: State<Boolean>,
isFabExpended: State<Boolean>, isFabExpended: State<Boolean>,
@ -148,127 +140,85 @@ private fun LexiconScreenContent(
onSearch: () -> Unit, onSearch: () -> Unit,
onItem: (LexiconItemUio) -> Unit, onItem: (LexiconItemUio) -> Unit,
) { ) {
Scaffold( Box(
modifier = modifier, modifier = modifier,
contentWindowInsets = NO_WINDOW_INSETS, ) {
topBar = { AnimatedContent(
TopAppBar( targetState = items.value.isEmpty(),
windowInsets = NO_WINDOW_INSETS, transitionSpec = { fadeIn() with fadeOut() },
title = { label = "AnimatedLexicon"
Text(text = stringResource(id = R.string.app_name)) ) { empty ->
when (empty) {
true -> LazyColumn(
modifier = Modifier
.fillMaxSize()
.pullRefresh(state = refreshState),
state = lazyColumnState,
contentPadding = paddingValues,
) {
items(count = 6) {
LexiconItem(
modifier = Modifier.cell(),
item = LexiconItemUio.preview(placeholder = true),
)
}
}
else -> LazyColumn(
modifier = Modifier
.fillMaxSize()
.pullRefresh(state = refreshState),
state = lazyColumnState,
contentPadding = paddingValues,
) {
items(
items = items.value,
key = { it.id },
contentType = { "Lexicon" },
) {
LexiconItem(
modifier = Modifier
.clickable { onItem(it) }
.cell(),
item = it,
)
}
}
}
}
AnimatedVisibility(
modifier = Modifier
.padding(all = 16.dp)
.align(Alignment.BottomEnd),
visible = items.value.isNotEmpty(),
enter = fadeIn(),
exit = fadeOut(),
) {
FloatingActionButton(
expended = isFabExpended.value,
onClick = onSearch,
colors = ButtonDefaults.outlinedButtonColors(
containerColor = MaterialTheme.colorScheme.surface,
),
icon = {
Icon(
painter = painterResource(id = R.drawable.ic_baseline_search_24),
contentDescription = null,
)
},
text = {
Text(text = stringResource(id = R.string.lexicon_search))
}, },
) )
},
floatingActionButton = {
Box(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.padding(start = 32.dp), // `Fix` Scaffold content size for FAB.
contentAlignment = Alignment.CenterEnd,
) {
AnimatedVisibility(
visible = items.value.isNotEmpty(),
enter = fadeIn(),
exit = fadeOut(),
) {
FloatingActionButton(
expended = isFabExpended.value,
onClick = onSearch,
icon = {
Icon(
painter = painterResource(id = R.drawable.ic_baseline_search_24),
contentDescription = null,
)
},
text = {
Text(text = stringResource(id = R.string.lexicon_search))
},
)
}
}
} }
) { padding ->
Box(
modifier = Modifier.padding(paddingValues = padding),
contentAlignment = Alignment.TopCenter,
) {
AnimatedContent(
targetState = items.value.isEmpty(),
transitionSpec = { fadeIn() with fadeOut() },
label = "AnimatedLexicon"
) { empty ->
when (empty) {
true -> LazyColumn(
modifier = Modifier
.fillMaxSize()
.pullRefresh(state = refreshState),
state = lazyColumnState,
contentPadding = PaddingValues(
top = 8.dp,
bottom = 8.dp + 16.dp + 56.dp + 16.dp,
),
) {
items(count = 6) {
LexiconItem(
modifier = Modifier.heightIn(min = MaterialTheme.lexicon.dimens.item),
item = LexiconItemUio.Brulkhai,
)
}
}
else -> LazyColumn( Loader(
modifier = Modifier modifier = Modifier.align(Alignment.TopCenter),
.fillMaxSize() refreshState = refreshState,
.pullRefresh(state = refreshState), refreshing = refreshing,
state = lazyColumnState,
contentPadding = PaddingValues(
top = 8.dp,
bottom = 8.dp + 16.dp + 56.dp + 16.dp,
),
) {
items(
items = items.value,
key = { it.id },
contentType = { "Lexicon" },
) {
LexiconItem(
modifier = Modifier
.clickable { onItem(it) }
.heightIn(min = MaterialTheme.lexicon.dimens.item),
item = it,
)
}
}
}
}
Loader(
refreshState = refreshState,
refreshing = refreshing,
)
}
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun Loader(
refreshState: PullRefreshState,
refreshing: State<Boolean>,
) {
if (refreshing.value) {
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.clip(shape = CircleShape)
) )
} }
PullRefreshIndicator(
refreshing = false,
state = refreshState,
)
} }
@Composable @Composable

View file

@ -1,4 +1,4 @@
package com.pixelized.rplexicon.ui.screens.lexicon package com.pixelized.rplexicon.ui.screens.lexicon.list
import android.util.Log import android.util.Log
import androidx.compose.runtime.State import androidx.compose.runtime.State
@ -9,7 +9,7 @@ import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecovera
import com.pixelized.rplexicon.R import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.model.Lexicon import com.pixelized.rplexicon.model.Lexicon
import com.pixelized.rplexicon.repository.LexiconRepository import com.pixelized.rplexicon.repository.LexiconRepository
import com.pixelized.rplexicon.repository.LexiconRepository.IncompatibleSheetStructure import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow

View file

@ -1,4 +1,4 @@
package com.pixelized.rplexicon.ui.screens.search package com.pixelized.rplexicon.ui.screens.lexicon.search
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement

View file

@ -1,4 +1,4 @@
package com.pixelized.rplexicon.ui.screens.search package com.pixelized.rplexicon.ui.screens.lexicon.search
import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
@ -47,7 +47,7 @@ import com.pixelized.rplexicon.ui.composable.form.DropDownFieldUio
import com.pixelized.rplexicon.ui.composable.form.TextField import com.pixelized.rplexicon.ui.composable.form.TextField
import com.pixelized.rplexicon.ui.composable.form.TextFieldUio import com.pixelized.rplexicon.ui.composable.form.TextFieldUio
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.navigation.screens.navigateToCharacterDetail import com.pixelized.rplexicon.ui.navigation.screens.navigateToLexiconDetail
import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.composable.stringResource import com.pixelized.rplexicon.utilitary.composable.stringResource
import com.pixelized.rplexicon.utilitary.extentions.lexicon import com.pixelized.rplexicon.utilitary.extentions.lexicon
@ -77,7 +77,7 @@ fun SearchScreen(
form = viewModel.form, form = viewModel.form,
onItem = { item -> onItem = { item ->
val form = viewModel.form val form = viewModel.form
screen.navigateToCharacterDetail( screen.navigateToLexiconDetail(
id = item.id, id = item.id,
highlight = form.search.value.value.takeIf { it.isNotEmpty() }, highlight = form.search.value.value.takeIf { it.isNotEmpty() },
race = form.race.value.value, race = form.race.value.value,

View file

@ -1,4 +1,4 @@
package com.pixelized.rplexicon.ui.screens.search package com.pixelized.rplexicon.ui.screens.lexicon.search
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf

View file

@ -0,0 +1,352 @@
package com.pixelized.rplexicon.ui.screens.quest.detail
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
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.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
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.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.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import java.lang.Integer.min
@Stable
data class QuestDetailUio(
val id: Int,
val title: String,
val steps: List<QuestStep>,
) {
@Stable
data class QuestStep(
val completed: Boolean,
val subtitle: String?,
val giver: String?,
val place: String?,
val individualReward: String?,
val globalReward: String?,
val description: String,
)
}
@Stable
data class AnnotatedQuestDetailUio(
val title: String,
val steps: List<AnnotatedQuestStep>,
) {
@Stable
data class AnnotatedQuestStep(
val completed: Boolean,
val subtitle: String?,
val giver: String?,
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(
title = title,
steps = annotatedSteps,
)
}
}
@Composable
@Stable
private fun QuestDetailUio.QuestStep.annotate(): AnnotatedQuestDetailUio.AnnotatedQuestStep {
val typography = MaterialTheme.lexicon.typography
return remember {
AnnotatedQuestDetailUio.AnnotatedQuestStep(
completed = completed,
subtitle = subtitle,
giver = giver,
place = place,
individualReward = individualReward,
globalReward = globalReward,
description = AnnotatedString(
text = description,
spanStyles = listOf(
AnnotatedString.Range(
item = typography.dropCapLargeSpan,
start = 0,
end = min(1, description.length),
)
)
),
)
}
}
@Composable
fun QuestDetailScreen(
viewModel: QuestDetailViewModel = hiltViewModel(),
) {
val screen = LocalScreenNavHost.current
Surface {
QuestDetailContent(
modifier = Modifier.fillMaxSize(),
item = viewModel.quest,
onBack = { screen.popBackStack() },
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun QuestDetailContent(
modifier: Modifier = Modifier,
item: State<QuestDetailUio>,
onBack: () -> Unit,
) {
val annotatedQuest = item.value.annotate()
Scaffold(
modifier = modifier,
containerColor = Color.Transparent,
topBar = {
TopAppBar(
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
painter = painterResource(id = R.drawable.ic_arrow_back_ios_new_24),
contentDescription = null
)
}
},
title = {
Text(text = stringResource(id = R.string.quest_detail_title))
},
)
},
content = { padding ->
LazyColumn(
modifier = Modifier.padding(padding),
contentPadding = PaddingValues(
top = 40.dp,
bottom = 16.dp,
start = 16.dp,
end = 16.dp
),
verticalArrangement = Arrangement.spacedBy(40.dp),
) {
item {
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,
)
}
}
items(annotatedQuest.steps) { quest ->
Column {
quest.subtitle?.let { subtitle ->
Row(
modifier = Modifier.fillMaxWidth(),
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.padding(all = 8.dp),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleLarge,
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,
)
}
}
quest.giver?.let {
Text(
modifier = Modifier.padding(top = 8.dp),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
text = "Commanditaire",
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = it,
)
}
quest.place?.let {
Text(
modifier = Modifier.padding(top = 8.dp),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
text = "Lieu",
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = it,
)
}
quest.globalReward?.let {
Text(
modifier = Modifier.padding(top = 8.dp),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
text = "Récompense de groupe",
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = it,
)
}
quest.individualReward?.let {
Text(
modifier = Modifier.padding(top = 8.dp),
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold,
text = "Récompense individuelle",
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = it,
)
}
Text(
modifier = Modifier.padding(top = 24.dp),
style = MaterialTheme.typography.bodyMedium,
text = quest.description,
)
}
}
}
},
)
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun QuestDetailPreview(
@PreviewParameter(QuestDetailPreviewProvider::class) preview: State<QuestDetailUio>,
) {
LexiconTheme {
Surface {
QuestDetailContent(
item = preview,
onBack = { },
)
}
}
}
private class QuestDetailPreviewProvider : PreviewParameterProvider<State<QuestDetailUio>> {
override val values: Sequence<State<QuestDetailUio>> = sequenceOf(
mutableStateOf(
QuestDetailUio(
id = 0,
title = "La chasse aux loups",
steps = listOf(
QuestDetailUio.QuestStep(
completed = false,
subtitle = "Partie 1",
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(
completed = false,
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.",
),
)
)
),
mutableStateOf(
QuestDetailUio(
id = 1,
title = "Les enfants de la caravanes",
steps = listOf(
QuestDetailUio.QuestStep(
completed = false,
subtitle = null,
giver = null,
place = null,
individualReward = "Pouvoir se regarder dans une glace",
globalReward = null,
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

@ -0,0 +1,41 @@
package com.pixelized.rplexicon.ui.screens.quest.detail
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import com.pixelized.rplexicon.repository.QuestRepository
import com.pixelized.rplexicon.ui.navigation.screens.questDetailArgument
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class QuestDetailViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
repository: QuestRepository,
) : ViewModel() {
val quest: State<QuestDetailUio>
init {
val argument = savedStateHandle.questDetailArgument
val source = repository.data.value[argument.id]
quest = mutableStateOf(
QuestDetailUio(
id = source.id,
title = source.title,
steps = source.entries.map { entry ->
QuestDetailUio.QuestStep(
completed = entry.complete,
subtitle = entry.subtitle,
giver = entry.questGiver,
place = entry.area,
individualReward = entry.individualReward,
globalReward = entry.groupReward,
description = entry.description,
)
},
)
)
}
}

View file

@ -0,0 +1,113 @@
package com.pixelized.rplexicon.ui.screens.quest.list
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.AnnotatedString
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.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.cell
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import com.pixelized.rplexicon.utilitary.extentions.placeholder
@Stable
data class QuestItemUio(
val id: Int,
val title: String,
val complete: Boolean,
val placeholder: Boolean = false,
) {
companion object {
fun preview(
id: Int = 0,
title: String = "La chasse aux loups",
complete: Boolean = false,
placeHolder: Boolean = false,
): QuestItemUio {
return QuestItemUio(
id = id,
title = title,
complete = complete,
placeholder = placeHolder,
)
}
}
}
@Composable
fun QuestItem(
modifier: Modifier = Modifier,
item: QuestItemUio,
) {
val typography = MaterialTheme.lexicon.typography
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
modifier = when (item.placeholder) {
true -> Modifier.placeholder { true }
else -> Modifier.alignByBaseline()
},
text = if (item.complete) "" else "",
)
Text(
modifier = when (item.placeholder) {
true -> Modifier.placeholder { true }
else -> Modifier.alignByBaseline()
},
text = remember(item.placeholder) {
AnnotatedString(
text = item.title,
spanStyles = when (item.placeholder) {
true -> emptyList()
else -> listOf(
AnnotatedString.Range(
item = typography.dropCapMediumSpan,
start = 0,
end = 1,
)
)
},
)
},
)
}
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun QuestItemPreview(
@PreviewParameter(QuestItemPreviewProvider::class) preview: QuestItemUio,
) {
LexiconTheme {
Surface {
QuestItem(
modifier = Modifier.cell(),
item = preview,
)
}
}
}
private class QuestItemPreviewProvider : PreviewParameterProvider<QuestItemUio> {
override val values: Sequence<QuestItemUio> = sequenceOf(
QuestItemUio.preview(complete = false, placeHolder = false),
QuestItemUio.preview(complete = true, placeHolder = false),
QuestItemUio.preview(complete = true, placeHolder = true),
)
}

View file

@ -0,0 +1,172 @@
package com.pixelized.rplexicon.ui.screens.quest.list
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.with
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.pullrefresh.PullRefreshState
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.ui.composable.Loader
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.navigation.screens.navigateToQuestDetail
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.cell
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun QuestListScreen(
viewModel: QuestListViewModel = hiltViewModel(),
) {
val scope = rememberCoroutineScope()
val lazyListState = rememberLazyListState()
val screen = LocalScreenNavHost.current
val refresh = rememberPullRefreshState(
refreshing = false,
onRefresh = {
scope.launch {
viewModel.fetchQuests()
}
},
)
Surface {
QuestListContent(
items = viewModel.items,
lazyColumnState = lazyListState,
refreshState = refresh,
refreshing = viewModel.isLoading,
onItem = {
screen.navigateToQuestDetail(id = it.id)
},
)
}
}
@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterialApi::class)
@Composable
private fun QuestListContent(
modifier: Modifier = Modifier,
lazyColumnState: LazyListState,
paddingValues: PaddingValues = PaddingValues(
top = 6.dp,
bottom = 8.dp + 16.dp + 56.dp + 16.dp,
),
refreshState: PullRefreshState,
refreshing: State<Boolean>,
items: State<List<QuestItemUio>>,
onItem: (QuestItemUio) -> Unit,
) {
Box(
modifier = modifier,
contentAlignment = Alignment.TopCenter,
) {
AnimatedContent(
targetState = items.value.isEmpty(),
transitionSpec = { fadeIn() with fadeOut() },
label = "AnimatedQuests"
) { empty ->
when (empty) {
true -> LazyColumn(
modifier = Modifier
.fillMaxSize()
.pullRefresh(state = refreshState),
state = lazyColumnState,
contentPadding = paddingValues,
) {
items(count = 2) {
QuestItem(
modifier = Modifier.cell(),
item = QuestItemUio.preview(placeHolder = true),
)
}
}
else -> LazyColumn(
modifier = Modifier
.fillMaxSize()
.pullRefresh(state = refreshState),
state = lazyColumnState,
contentPadding = paddingValues,
) {
items(
items = items.value,
key = { it.id },
contentType = { "Lexicon" },
) {
QuestItem(
modifier = Modifier
.clickable { onItem(it) }
.cell(),
item = it,
)
}
}
}
}
Loader(
refreshState = refreshState,
refreshing = refreshing,
)
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun QuestListPreview() {
LexiconTheme {
Surface {
QuestListContent(
modifier = Modifier.fillMaxSize(),
lazyColumnState = rememberLazyListState(),
refreshState = rememberPullRefreshState(
refreshing = false,
onRefresh = {},
),
refreshing = remember { mutableStateOf(false) },
items = remember {
mutableStateOf(
listOf(
QuestItemUio.preview(id = 0, title = "La chasse aux loups"),
QuestItemUio.preview(id = 1, title = "Les enfants de la caravanes"),
)
)
},
onItem = { },
)
}
}
}

View file

@ -0,0 +1,75 @@
package com.pixelized.rplexicon.ui.screens.quest.list
import android.util.Log
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException
import com.pixelized.rplexicon.repository.QuestRepository
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class QuestListViewModel @Inject constructor(
private val repository: QuestRepository,
) : ViewModel() {
private val _isLoading = mutableStateOf(false)
val isLoading: State<Boolean> get() = _isLoading
private val _items = mutableStateOf<List<QuestItemUio>>(emptyList())
val items: State<List<QuestItemUio>> get() = _items
// private val _error = MutableSharedFlow<LexiconErrorUio>()
// val error: SharedFlow<LexiconErrorUio> get() = _error
init {
viewModelScope.launch {
launch {
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 }
}
}
launch {
fetchQuests()
}
}
}
suspend fun fetchQuests() {
try {
_isLoading.value = true
repository.fetchQuests()
}
// user need to accept OAuth2 permission.
catch (exception: UserRecoverableAuthIOException) {
Log.e(TAG, exception.message, exception)
// _error.emit(LexiconErrorUio.Permission(intent = exception.intent))
} catch (exception: IncompatibleSheetStructure) {
Log.e(TAG, exception.message, exception)
// _error.emit(LexiconErrorUio.Structure)
}
// default exception
catch (exception: Exception) {
Log.e(TAG, exception.message, exception)
// _error.emit(LexiconErrorUio.Default)
}
// clean the laoding state
finally {
_isLoading.value = false
}
}
companion object {
const val TAG = "QuestListViewModel"
}
}

View file

@ -19,6 +19,8 @@ import com.pixelized.rplexicon.ui.theme.dimen.LexiconDimens
import com.pixelized.rplexicon.ui.theme.dimen.lexiconDimen import com.pixelized.rplexicon.ui.theme.dimen.lexiconDimen
import com.pixelized.rplexicon.ui.theme.shape.LexiconShapes import com.pixelized.rplexicon.ui.theme.shape.LexiconShapes
import com.pixelized.rplexicon.ui.theme.shape.lexiconShapes import com.pixelized.rplexicon.ui.theme.shape.lexiconShapes
import com.pixelized.rplexicon.ui.theme.typography.LexiconTypography
import com.pixelized.rplexicon.ui.theme.typography.lexiconTypography
val LocalLexiconTheme = compositionLocalOf<LexiconTheme> { val LocalLexiconTheme = compositionLocalOf<LexiconTheme> {
error("LocalLexiconTheme not ready yet.") error("LocalLexiconTheme not ready yet.")
@ -29,6 +31,7 @@ data class LexiconTheme(
val colorScheme: LexiconColors, val colorScheme: LexiconColors,
val shapes: LexiconShapes, val shapes: LexiconShapes,
val dimens: LexiconDimens, val dimens: LexiconDimens,
val typography: LexiconTypography,
) )
@Composable @Composable
@ -44,6 +47,7 @@ fun LexiconTheme(
}, },
shapes = lexiconShapes(), shapes = lexiconShapes(),
dimens = lexiconDimen(), dimens = lexiconDimen(),
typography = lexiconTypography(),
) )
} }
@ -60,14 +64,16 @@ fun LexiconTheme(
} }
} }
CompositionLocalProvider( MaterialTheme(
LocalLexiconTheme provides lexiconTheme, colorScheme = lexiconTheme.colorScheme.base,
) { shapes = lexiconTheme.shapes.base,
MaterialTheme( typography = lexiconTheme.typography.base,
colorScheme = lexiconTheme.colorScheme.base, content = {
shapes = lexiconTheme.shapes.base, CompositionLocalProvider(
typography = Typography, LocalLexiconTheme provides lexiconTheme,
content = content ) {
) content()
} }
}
)
} }

View file

@ -1,34 +0,0 @@
package com.pixelized.rplexicon.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)

View file

@ -12,7 +12,7 @@ data class LexiconDimens(
) )
fun lexiconDimen( fun lexiconDimen(
item: Dp = 48.dp item: Dp = 52.dp
) = LexiconDimens( ) = LexiconDimens(
item = item, item = item,
) )

View file

@ -0,0 +1,38 @@
package com.pixelized.rplexicon.ui.theme.typography
import androidx.compose.material3.Typography
import androidx.compose.runtime.Stable
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.BaselineShift
import androidx.compose.ui.unit.sp
import com.pixelized.rplexicon.R
@Stable
val regal = FontFamily(
Font(resId = R.font.regal, weight = FontWeight.Normal),
)
@Suppress("MemberVisibilityCanBePrivate")
@Stable
class LexiconTypography(
val base: Typography = Typography(),
val dropCapMedium: TextStyle = base.displaySmall.copy(
fontFamily = regal,
baselineShift = BaselineShift(-0.3f),
letterSpacing = (-6).sp
),
val dropCapLarge: TextStyle = base.displayMedium.copy(
fontFamily = regal,
baselineShift = BaselineShift.Subscript,
letterSpacing = (-8).sp
),
) {
val dropCapMediumSpan: SpanStyle = dropCapMedium.toSpanStyle()
val dropCapLargeSpan: SpanStyle = dropCapLarge.toSpanStyle()
}
fun lexiconTypography() = LexiconTypography()

View file

@ -0,0 +1,3 @@
package com.pixelized.rplexicon.utilitary.exceptions
class IncompatibleSheetStructure(message: String?) : Exception(message)

View file

@ -0,0 +1,3 @@
package com.pixelized.rplexicon.utilitary.exceptions
class ServiceNotReady : Exception()

View file

@ -3,12 +3,17 @@ package com.pixelized.rplexicon.utilitary.extentions
import androidx.compose.animation.core.FiniteAnimationSpec import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.Transition import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.spring import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.dp
import com.google.accompanist.placeholder.PlaceholderHighlight import com.google.accompanist.placeholder.PlaceholderHighlight
import com.google.accompanist.placeholder.placeholder import com.google.accompanist.placeholder.placeholder
@ -20,11 +25,21 @@ fun Modifier.placeholder(
placeholderFadeTransitionSpec: @Composable Transition.Segment<Boolean>.() -> FiniteAnimationSpec<Float> = { spring() }, placeholderFadeTransitionSpec: @Composable Transition.Segment<Boolean>.() -> FiniteAnimationSpec<Float> = { spring() },
contentFadeTransitionSpec: @Composable Transition.Segment<Boolean>.() -> FiniteAnimationSpec<Float> = { spring() }, contentFadeTransitionSpec: @Composable Transition.Segment<Boolean>.() -> FiniteAnimationSpec<Float> = { spring() },
visible: () -> Boolean, visible: () -> Boolean,
): Modifier = placeholder( ): Modifier = composed {
visible = visible(), placeholder(
color = color, visible = visible(),
shape = shape, color = color,
highlight = highlight, shape = shape,
placeholderFadeTransitionSpec = placeholderFadeTransitionSpec, highlight = highlight,
contentFadeTransitionSpec = contentFadeTransitionSpec, placeholderFadeTransitionSpec = placeholderFadeTransitionSpec,
) contentFadeTransitionSpec = contentFadeTransitionSpec,
)
}
@Composable
fun Modifier.cell() = composed {
Modifier
.fillMaxWidth()
.heightIn(min = MaterialTheme.lexicon.dimens.item)
.padding(horizontal = 16.dp, vertical = 4.dp)
}

View file

@ -0,0 +1,8 @@
package com.pixelized.rplexicon.utilitary.extentions
fun MutableCollection<Any>?.sheet(): List<*>? {
return this?.firstOrNull {
val sheet = it as? ArrayList<*>
sheet != null
} as List<*>?
}

View file

@ -20,18 +20,18 @@ fun Regex?.foldAll(
} }
}?.takeIf { it.isNotEmpty() }?.let { "$it..." } }?.takeIf { it.isNotEmpty() }?.let { "$it..." }
fun Regex.annotatedSpan( fun Regex?.annotatedSpan(
input: String, input: String,
startIndex: Int = 0, startIndex: Int = 0,
spanStyle: SpanStyle spanStyle: SpanStyle
): List<AnnotatedString.Range<SpanStyle>> { ): List<AnnotatedString.Range<SpanStyle>> {
return findAll(input = input, startIndex = startIndex).map { return this?.findAll(input = input, startIndex = startIndex)?.map {
AnnotatedString.Range( AnnotatedString.Range(
item = spanStyle, item = spanStyle,
start = it.range.first, start = it.range.first,
end = it.range.last + 1 end = it.range.last + 1
) )
}.toList() }?.toList() ?: emptyList()
} }
fun Regex.annotatedString( fun Regex.annotatedString(

View file

@ -0,0 +1,30 @@
package com.pixelized.rplexicon.utilitary.extentions
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
@Throws(IncompatibleSheetStructure::class)
fun Any?.checkSheetStructure(model: List<String>): HashMap<String, Int> {
// check if the row is a list
if (this !is ArrayList<*>) {
throw IncompatibleSheetStructure("First row is not a List: $this")
}
// parse the first line to find element that we recognize.
val sheetStructure = hashMapOf<String, Int>()
forEachIndexed { index, cell ->
if (cell is String && model.contains(cell)) {
sheetStructure[cell] = index
}
}
// check if we found everything we need.
when {
sheetStructure.size < model.size -> throw IncompatibleSheetStructure(
message = "Sheet header row does not have enough column: $size.\nstructure: $this\nheader: $sheetStructure"
)
sheetStructure.size > model.size -> throw IncompatibleSheetStructure(
message = "Sheet header row does have too mush columns: $size.\nstructure: $this\nheader: $sheetStructure"
)
}
return sheetStructure
}

View file

@ -0,0 +1,10 @@
<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

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,6 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM7.35,18.5C8.66,17.56 10.26,17 12,17s3.34,0.56 4.65,1.5C15.34,19.44 13.74,20 12,20S8.66,19.44 7.35,18.5zM18.14,17.12L18.14,17.12C16.45,15.8 14.32,15 12,15s-4.45,0.8 -6.14,2.12l0,0C4.7,15.73 4,13.95 4,12c0,-4.42 3.58,-8 8,-8s8,3.58 8,8C20,13.95 19.3,15.73 18.14,17.12z"/>
<path android:fillColor="@android:color/white" android:pathData="M12,6c-1.93,0 -3.5,1.57 -3.5,3.5S10.07,13 12,13s3.5,-1.57 3.5,-3.5S13.93,6 12,6zM12,11c-0.83,0 -1.5,-0.67 -1.5,-1.5S11.17,8 12,8s1.5,0.67 1.5,1.5S12.83,11 12,11z"/>
</vector>

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M20.5,3l-0.16,0.03L15,5.1 9,3 3.36,4.9c-0.21,0.07 -0.36,0.25 -0.36,0.48L3,20.5c0,0.28 0.22,0.5 0.5,0.5l0.16,-0.03L9,18.9l6,2.1 5.64,-1.9c0.21,-0.07 0.36,-0.25 0.36,-0.48L21,3.5c0,-0.28 -0.22,-0.5 -0.5,-0.5zM10,5.47l4,1.4v11.66l-4,-1.4L10,5.47zM5,6.46l3,-1.01v11.7l-3,1.16L5,6.46zM19,17.54l-3,1.01L16,6.86l3,-1.16v11.84z"/>
</vector>

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 904 B

After

Width:  |  Height:  |  Size: 1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Before After
Before After

View file

@ -1,5 +1,5 @@
<resources> <resources>
<string name="app_name">Rp-Lexique</string> <string name="app_name">Rp-Compagnon</string>
<string name="error_generic">Ah !? y\'a un truc qui foire quelque part.</string> <string name="error_generic">Ah !? y\'a un truc qui foire quelque part.</string>
<string name="error_structure">La structure du fichier semble avoir changé et n\'est plus compatible avec cette application.</string> <string name="error_structure">La structure du fichier semble avoir changé et n\'est plus compatible avec cette application.</string>
@ -28,6 +28,9 @@
<string name="action_google_sign_in">Se connecter avec</string> <string name="action_google_sign_in">Se connecter avec</string>
<string name="home_lexicon">Lexique</string>
<string name="home_quest_log">Journal de quêtes</string>
<string name="lexicon_search">Rechercher</string> <string name="lexicon_search">Rechercher</string>
<string name="detail_title">Détails du personnage</string> <string name="detail_title">Détails du personnage</string>
@ -42,4 +45,6 @@
<string name="search_item_description">Description :</string> <string name="search_item_description">Description :</string>
<string name="search_item_history">Histoire :</string> <string name="search_item_history">Histoire :</string>
<string name="search_item_tags">Mots clés :</string> <string name="search_item_tags">Mots clés :</string>
<string name="quest_detail_title">Détails de quête</string>
</resources> </resources>

View file

@ -1,5 +1,5 @@
<resources> <resources>
<string name="app_name">Rp-Lexicon</string> <string name="app_name">Rp-Companion</string>
<string name="error_generic">Oups, it should not be rocket science.</string> <string name="error_generic">Oups, it should not be rocket science.</string>
<string name="error_structure">The file structure appears to have changed and is no longer compatible with this application</string> <string name="error_structure">The file structure appears to have changed and is no longer compatible with this application</string>
@ -28,6 +28,9 @@
<string name="action_google_sign_in">Sign in with</string> <string name="action_google_sign_in">Sign in with</string>
<string name="home_lexicon">Lexicon</string>
<string name="home_quest_log">Quest logs</string>
<string name="lexicon_search">Search</string> <string name="lexicon_search">Search</string>
<string name="detail_title">Character\'s details</string> <string name="detail_title">Character\'s details</string>
@ -42,4 +45,6 @@
<string name="search_item_description">Description:</string> <string name="search_item_description">Description:</string>
<string name="search_item_history">History:</string> <string name="search_item_history">History:</string>
<string name="search_item_tags">Tags:</string> <string name="search_item_tags">Tags:</string>
<string name="quest_detail_title">Quest details</string>
</resources> </resources>