diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6f1b495..68740ea 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,6 +11,7 @@ android:allowBackup="true" android:colorMode="wideColorGamut" android:dataExtractionRules="@xml/data_extraction_rules" + android:enableOnBackInvokedCallback="true" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png index b52cee2..1ec6937 100644 Binary files a/app/src/main/ic_launcher-playstore.png and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/pixelized/rplexicon/model/QuestEntry.kt b/app/src/main/java/com/pixelized/rplexicon/model/QuestEntry.kt new file mode 100644 index 0000000..3ab9693 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/model/QuestEntry.kt @@ -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, +) + +@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, +) \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/repository/LexiconRepository.kt b/app/src/main/java/com/pixelized/rplexicon/repository/LexiconRepository.kt index 7afac05..b4e7c0c 100644 --- a/app/src/main/java/com/pixelized/rplexicon/repository/LexiconRepository.kt +++ b/app/src/main/java/com/pixelized/rplexicon/repository/LexiconRepository.kt @@ -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.model.ValueRange 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.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -59,7 +63,7 @@ class LexiconRepository @Inject constructor( val lexicon: List = sheet?.mapIndexedNotNull { index, row -> when { index == 0 -> { - sheetStructure = checkSheetStructure(firstRow = row) + sheetStructure = row.checkSheetStructure(model = Sheet.COLUMNS) null } @@ -79,34 +83,6 @@ class LexiconRepository @Inject constructor( _data.tryEmit(lexicon) } - @Throws(IncompatibleSheetStructure::class) - private fun checkSheetStructure(firstRow: Any?): HashMap { - // 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() - - 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( sheetStructure: Map?, id: Int, @@ -159,13 +135,6 @@ class LexiconRepository @Inject constructor( } } - private fun MutableCollection?.sheet(): List<*>? { - return this?.firstOrNull { - val sheet = it as? ArrayList<*> - sheet != null - } as List<*>? - } - private fun String?.toUriOrNull(): Uri? = try { this?.takeIf { it.isNotBlank() }?.toUri() } catch (_: Exception) { @@ -181,10 +150,6 @@ class LexiconRepository @Inject constructor( private val Map?.history: Int get() = this?.getValue(Sheet.HISTORY) ?: 6 private val Map?.tags: Int get() = this?.getValue(Sheet.TAGS) ?: 7 - class ServiceNotReady : Exception() - - class IncompatibleSheetStructure(message: String?) : Exception(message) - companion object { const val TAG = "LexiconRepository" } diff --git a/app/src/main/java/com/pixelized/rplexicon/repository/QuestRepository.kt b/app/src/main/java/com/pixelized/rplexicon/repository/QuestRepository.kt new file mode 100644 index 0000000..6abe23a --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/repository/QuestRepository.kt @@ -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>(emptyList()) + val data: StateFlow> 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? = null + + val questEntries: List = 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?, + 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?.title: Int get() = this?.getValue(Sheet.TITLE) ?: 0 + private val Map?.subtitle: Int get() = this?.getValue(Sheet.SUBTITLE) ?: 1 + private val Map?.complete: Int get() = this?.getValue(Sheet.COMPLETE) ?: 2 + private val Map?.questGiver: Int get() = this?.getValue(Sheet.QUEST_GIVER) ?: 3 + private val Map?.area: Int get() = this?.getValue(Sheet.AREA) ?: 4 + private val Map?.groupReward: Int get() = this?.getValue(Sheet.G_REWARD) ?: 5 + private val Map?.individualReward: Int get() = this?.getValue(Sheet.I_REWARD) ?: 6 + private val Map?.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] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/composable/Loader.kt b/app/src/main/java/com/pixelized/rplexicon/ui/composable/Loader.kt new file mode 100644 index 0000000..879e914 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/composable/Loader.kt @@ -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, +) { + if (refreshing.value) { + LinearProgressIndicator( + modifier = modifier + .fillMaxWidth() + .clip(shape = CircleShape) + ) + } + + PullRefreshIndicator( + modifier = modifier, + refreshing = false, + state = refreshState, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/HomeNavHost.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/HomeNavHost.kt new file mode 100644 index 0000000..8f7c535 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/HomeNavHost.kt @@ -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 { + error("LocalScreenNavHost not ready") +} + +@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class) +@Composable +fun HomeNavHost( + lexiconListState: LazyListState, + navHostController: NavHostController = rememberAnimatedNavController(), + bottomBarItems: List = 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, + val icon: Int, + val label: Int, + val onClick: () -> Unit, +) + +@Composable +@Stable +private fun rememberBottomBarItems( + navHostController: NavHostController, +): List { + 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()) } + ), + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/ScreenNavHost.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/ScreenNavHost.kt index f953202..58878b1 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/ScreenNavHost.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/ScreenNavHost.kt @@ -1,19 +1,22 @@ package com.pixelized.rplexicon.ui.navigation import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.staticCompositionLocalOf +import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import androidx.navigation.NavOptionsBuilder import com.google.accompanist.navigation.animation.AnimatedNavHost import com.google.accompanist.navigation.animation.rememberAnimatedNavController import com.pixelized.rplexicon.ui.navigation.screens.AUTHENTICATION_ROUTE import com.pixelized.rplexicon.ui.navigation.screens.composableAuthentication -import com.pixelized.rplexicon.ui.navigation.screens.composableCharacterDetail -import com.pixelized.rplexicon.ui.navigation.screens.composableLexicon -import com.pixelized.rplexicon.ui.navigation.screens.composableSearch +import com.pixelized.rplexicon.ui.navigation.screens.composableHome +import com.pixelized.rplexicon.ui.navigation.screens.composableLexiconDetail +import com.pixelized.rplexicon.ui.navigation.screens.composableLexiconSearch +import com.pixelized.rplexicon.ui.navigation.screens.composableQuestDetail val LocalScreenNavHost = staticCompositionLocalOf { error("LocalScreenNavHost not ready") @@ -25,7 +28,7 @@ fun ScreenNavHost( navHostController: NavHostController = rememberAnimatedNavController(), startDestination: String = AUTHENTICATION_ROUTE, ) { - val lexiconListState = rememberLazyListState() + val lexiconListState: LazyListState = rememberLazyListState() CompositionLocalProvider( LocalScreenNavHost provides navHostController, @@ -35,9 +38,12 @@ fun ScreenNavHost( startDestination = startDestination, ) { composableAuthentication() - composableLexicon(lazyListState = lexiconListState) - composableCharacterDetail() - composableSearch() + composableHome(lexiconListState) + + composableLexiconDetail() + composableLexiconSearch() + + composableQuestDetail() } } } @@ -50,3 +56,15 @@ fun rootOption(): NavOptionsBuilder.() -> Unit = { 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 +} diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableLexicon.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/pages/ComposableLexicon.kt similarity index 81% rename from app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableLexicon.kt rename to app/src/main/java/com/pixelized/rplexicon/ui/navigation/pages/ComposableLexicon.kt index 4ed1b19..b068fd3 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableLexicon.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/pages/ComposableLexicon.kt @@ -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.navigation.NavGraphBuilder @@ -6,7 +6,7 @@ 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.lexicon.LexiconScreen +import com.pixelized.rplexicon.ui.screens.lexicon.list.LexiconScreen private const val ROUTE = "lexicon" @@ -17,7 +17,7 @@ fun NavGraphBuilder.composableLexicon( ) { animatedComposable( route = LEXICON_ROUTE, - animation = NavigationAnimation.Fade, + animation = NavigationAnimation.NONE, ) { LexiconScreen( lazyListState = lazyListState diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/pages/ComposableQuestList.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/pages/ComposableQuestList.kt new file mode 100644 index 0000000..a6d6919 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/pages/ComposableQuestList.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableHome.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableHome.kt new file mode 100644 index 0000000..c554a18 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableHome.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableCharacterDetail.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableLexiconDetail.kt similarity index 84% rename from app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableCharacterDetail.kt rename to app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableLexiconDetail.kt index d31e236..a95f936 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableCharacterDetail.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableLexiconDetail.kt @@ -11,10 +11,10 @@ import androidx.navigation.navArgument import com.pixelized.rplexicon.model.Lexicon import com.pixelized.rplexicon.ui.navigation.NavigationAnimation 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 -private const val ROUTE = "CharacterDetail" +private const val ROUTE = "LexiconDetail" private const val ARG_ID = "id" private const val ARG_HIGHLIGHT = "highlight" 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_HIGHLIGHT_GENDER = "highlightGender" -//CharacterDetail -// ?id=0 -// &highlight=null -// &race=false -// &highlightRace=UNDETERMINED -// &gender=false -// &highlightGender=UNDETERMINED - - -val CHARACTER_DETAIL_ROUTE = ROUTE + +val LEXICON_DETAIL_ROUTE = ROUTE + "?${ARG_ID.ARG}" + "&${ARG_HIGHLIGHT.ARG}" + "&${ARG_RACE.ARG}" + @@ -41,7 +32,7 @@ val CHARACTER_DETAIL_ROUTE = ROUTE + @Stable @Immutable -data class CharacterDetailArgument( +data class LexiconDetailArgument( val id: Int, val highlight: String?, val race: Lexicon.Race, @@ -50,8 +41,8 @@ data class CharacterDetailArgument( val highlightGender: Boolean, ) -val SavedStateHandle.characterDetailArgument: CharacterDetailArgument - get() = CharacterDetailArgument( +val SavedStateHandle.lexiconDetailArgument: LexiconDetailArgument + get() = LexiconDetailArgument( id = get(ARG_ID) ?: error("CharacterDetailArgument argument: $ARG_ID"), race = get(ARG_RACE) @@ -65,9 +56,9 @@ val SavedStateHandle.characterDetailArgument: CharacterDetailArgument highlight = get(ARG_HIGHLIGHT), ) -fun NavGraphBuilder.composableCharacterDetail() { +fun NavGraphBuilder.composableLexiconDetail() { animatedComposable( - route = CHARACTER_DETAIL_ROUTE, + route = LEXICON_DETAIL_ROUTE, arguments = listOf( navArgument(name = ARG_ID) { type = NavType.IntType @@ -91,11 +82,11 @@ fun NavGraphBuilder.composableCharacterDetail() { ), animation = NavigationAnimation.Push, ) { - CharacterDetailScreen() + LexiconDetailScreen() } } -fun NavHostController.navigateToCharacterDetail( +fun NavHostController.navigateToLexiconDetail( id: Int, highlight: String? = null, race: Lexicon.Race? = null, diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableSearch.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableLexiconSearch.kt similarity index 79% rename from app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableSearch.kt rename to app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableLexiconSearch.kt index cfc4cf9..36c0fc3 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableSearch.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableLexiconSearch.kt @@ -5,13 +5,13 @@ 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.search.SearchScreen +import com.pixelized.rplexicon.ui.screens.lexicon.search.SearchScreen private const val ROUTE = "search" const val SEARCH_ROUTE = ROUTE -fun NavGraphBuilder.composableSearch() { +fun NavGraphBuilder.composableLexiconSearch() { animatedComposable( route = SEARCH_ROUTE, animation = NavigationAnimation.Push, @@ -20,7 +20,7 @@ fun NavGraphBuilder.composableSearch() { } } -fun NavHostController.navigateToSearch( +fun NavHostController.navigateToLexiconSearch( option: NavOptionsBuilder.() -> Unit = {}, ) { navigate(route = ROUTE, builder = option) diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableQuestDetail.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableQuestDetail.kt new file mode 100644 index 0000000..4500b5c --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/screens/ComposableQuestDetail.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/authentication/AuthenticationScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/authentication/AuthenticationScreen.kt index 494223f..3af4278 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/authentication/AuthenticationScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/authentication/AuthenticationScreen.kt @@ -52,7 +52,7 @@ import com.pixelized.rplexicon.LocalActivity import com.pixelized.rplexicon.R import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost 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.colors.GoogleColorPalette import com.pixelized.rplexicon.utilitary.sensor.Gyroscope @@ -88,7 +88,7 @@ fun AuthenticationScreen( HandleAuthenticationState( state = state, onSignIn = { - screen.navigateToLexicon(option = rootOption()) + screen.navigateToHome(option = rootOption()) }, ) } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconItem.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconItem.kt deleted file mode 100644 index 0af7cb3..0000000 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconItem.kt +++ /dev/null @@ -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, - ) - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/detail/LexiconDetailScreen.kt similarity index 93% rename from app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailScreen.kt rename to app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/detail/LexiconDetailScreen.kt index 74ea444..249c803 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/detail/LexiconDetailScreen.kt @@ -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_YES @@ -69,7 +69,7 @@ import com.pixelized.rplexicon.utilitary.extentions.highlightRegex import com.skydoves.landscapist.ImageOptions @Stable -data class CharacterDetailUio( +data class LexiconDetailUio( val name: String, val diminutive: String?, val gender: Lexicon.Gender, @@ -84,7 +84,7 @@ data class CharacterDetailUio( ) @Stable -data class AnnotatedCharacterDetailUio( +data class AnnotatedLexiconDetailUio( val name: AnnotatedString, val diminutive: AnnotatedString?, val gender: AnnotatedString, @@ -97,7 +97,7 @@ data class AnnotatedCharacterDetailUio( @Composable @Stable -fun CharacterDetailUio.annotated(): AnnotatedCharacterDetailUio { +fun LexiconDetailUio.annotate(): AnnotatedLexiconDetailUio { val colorScheme = MaterialTheme.colorScheme val highlight = remember { SpanStyle(color = colorScheme.primary) } val highlightRegex = remember(search) { search.highlightRegex } @@ -105,7 +105,7 @@ fun CharacterDetailUio.annotated(): AnnotatedCharacterDetailUio { val race = stringResource(id = race) return remember(search, race, highlightRace, gender, highlightGender) { - AnnotatedCharacterDetailUio( + AnnotatedLexiconDetailUio( portrait = portrait, name = AnnotatedString( text = name, @@ -148,13 +148,13 @@ fun CharacterDetailUio.annotated(): AnnotatedCharacterDetailUio { } @Composable -fun CharacterDetailScreen( - viewModel: CharacterDetailViewModel = hiltViewModel(), +fun LexiconDetailScreen( + viewModel: LexiconDetailViewModel = hiltViewModel(), ) { val screen = LocalScreenNavHost.current Surface { - CharacterDetailScreenContent( + LexiconDetailContent( modifier = Modifier.fillMaxSize(), item = viewModel.character, onBack = { screen.popBackStack() }, @@ -164,15 +164,15 @@ fun CharacterDetailScreen( @OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) @Composable -private fun CharacterDetailScreenContent( +private fun LexiconDetailContent( modifier: Modifier = Modifier, state: ScrollState = rememberScrollState(), - item: State, + item: State, onBack: () -> Unit, ) { val colorScheme = MaterialTheme.colorScheme val typography = MaterialTheme.typography - val annotatedItem = item.value.annotated() + val annotatedItem = item.value.annotate() Scaffold( modifier = modifier, @@ -244,7 +244,8 @@ private fun CharacterDetailScreenContent( annotatedItem.diminutive?.let { Text( modifier = Modifier.alignByBaseline(), - style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) }, + style = typography.labelMedium, + fontStyle = FontStyle.Italic, text = it, ) } @@ -255,11 +256,13 @@ private fun CharacterDetailScreenContent( horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Text( - style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) }, + style = typography.labelMedium, + fontStyle = FontStyle.Italic, text = annotatedItem.gender, ) Text( - style = remember { typography.labelMedium.copy(fontStyle = FontStyle.Italic) }, + style = typography.labelMedium, + fontStyle = FontStyle.Italic, text = annotatedItem.race, ) } @@ -329,7 +332,8 @@ private fun CharacterDetailScreenContent( annotatedItem.tags?.let { Text( 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, ) } @@ -374,14 +378,14 @@ private fun Modifier.scrollOffset( } @Composable -@Preview(uiMode = UI_MODE_NIGHT_NO) -@Preview(uiMode = UI_MODE_NIGHT_YES) -private fun CharacterDetailScreenContentPreview() { +@Preview(uiMode = UI_MODE_NIGHT_NO, heightDp = 980) +@Preview(uiMode = UI_MODE_NIGHT_YES, heightDp = 980) +private fun LexiconDetailPreview() { LexiconTheme { Surface { val character = remember { mutableStateOf( - CharacterDetailUio( + LexiconDetailUio( name = "Brulkhai", diminutive = "./ Bru", 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 lorsqu’elle est en bonne santé. Elle a les cheveux châtains, les yeux noisettes et la peau couleur gris-vert typique de son espèce. D’un 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 qu’elle considère plus faibles qu’elle. D’une 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 d’un manque d’éducation et d’une capacité limitée à gérer ses émotions qu’à une débilité congénitale. Elle voue à la force un culte car c’est par son expression qu’elle se sent vraiment vivante et éprouve de grandes difficultés vis à vis de ceux qu’elle nomme foshnu (bébé, chouineur en commun).", history = null, - tags = null, + tags = "protagoniste, brute", search = "Bru", highlightGender = true, highlightRace = true, ) ) } - CharacterDetailScreenContent( + LexiconDetailContent( modifier = Modifier.fillMaxSize(), item = character, onBack = { }, diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/detail/LexiconDetailViewModel.kt similarity index 77% rename from app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailViewModel.kt rename to app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/detail/LexiconDetailViewModel.kt index 60bd6b5..bf5b3c8 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/detail/CharacterDetailViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/detail/LexiconDetailViewModel.kt @@ -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.mutableStateOf import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel 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 javax.inject.Inject @HiltViewModel -class CharacterDetailViewModel @Inject constructor( +class LexiconDetailViewModel @Inject constructor( savedStateHandle: SavedStateHandle, repository: LexiconRepository, ) : ViewModel() { - val character: State + val character: State init { - val argument = savedStateHandle.characterDetailArgument + val argument = savedStateHandle.lexiconDetailArgument val source = repository.data.value[argument.id] character = mutableStateOf( - CharacterDetailUio( + LexiconDetailUio( name = source.name, diminutive = source.diminutive?.let { "./ $it" }, gender = source.gender, diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/list/LexiconItem.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/list/LexiconItem.kt new file mode 100644 index 0000000..9f926fa --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/list/LexiconItem.kt @@ -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 { + override val values: Sequence = 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, + ), + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/list/LexiconScreen.kt similarity index 56% rename from app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconScreen.kt rename to app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/list/LexiconScreen.kt index 947af39..f3a2397 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/list/LexiconScreen.kt @@ -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.content.Intent @@ -16,29 +16,20 @@ 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.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -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.foundation.shape.CircleShape 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.pullRefresh 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.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.LaunchedEffect import androidx.compose.runtime.Stable @@ -49,7 +40,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource 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.hilt.navigation.compose.hiltViewModel import com.pixelized.rplexicon.LocalSnack -import com.pixelized.rplexicon.NO_WINDOW_INSETS import com.pixelized.rplexicon.R 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.screens.navigateToCharacterDetail -import com.pixelized.rplexicon.ui.navigation.screens.navigateToSearch -import com.pixelized.rplexicon.ui.screens.lexicon.LexiconErrorUio.Default -import com.pixelized.rplexicon.ui.screens.lexicon.LexiconErrorUio.Permission -import com.pixelized.rplexicon.ui.screens.lexicon.LexiconErrorUio.Structure +import com.pixelized.rplexicon.ui.navigation.screens.navigateToLexiconDetail +import com.pixelized.rplexicon.ui.navigation.screens.navigateToLexiconSearch +import com.pixelized.rplexicon.ui.screens.lexicon.list.LexiconErrorUio.Default +import com.pixelized.rplexicon.ui.screens.lexicon.list.LexiconErrorUio.Permission +import com.pixelized.rplexicon.ui.screens.lexicon.list.LexiconErrorUio.Structure 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.launch @@ -109,17 +99,16 @@ fun LexiconScreen( Surface { LexiconScreenContent( - modifier = Modifier.systemBarsPadding(), items = viewModel.items, lazyColumnState = lazyListState, refreshState = refresh, refreshing = viewModel.isLoading, isFabExpended = isFabExpended, onSearch = { - screen.navigateToSearch() + screen.navigateToLexiconSearch() }, onItem = { - screen.navigateToCharacterDetail(id = it.id) + screen.navigateToLexiconDetail(id = it.id) }, ) @@ -133,7 +122,6 @@ fun LexiconScreen( } @OptIn( - ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class, ExperimentalAnimationApi::class, ) @@ -141,6 +129,10 @@ fun LexiconScreen( private fun LexiconScreenContent( modifier: Modifier = Modifier, lazyColumnState: LazyListState, + paddingValues: PaddingValues = PaddingValues( + top = 6.dp, + bottom = 8.dp + 16.dp + 56.dp + 16.dp, + ), refreshState: PullRefreshState, refreshing: State, isFabExpended: State, @@ -148,127 +140,85 @@ private fun LexiconScreenContent( onSearch: () -> Unit, onItem: (LexiconItemUio) -> Unit, ) { - Scaffold( + Box( modifier = modifier, - contentWindowInsets = NO_WINDOW_INSETS, - topBar = { - TopAppBar( - windowInsets = NO_WINDOW_INSETS, - title = { - Text(text = stringResource(id = R.string.app_name)) + ) { + 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, + ) { + 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( - modifier = Modifier - .fillMaxSize() - .pullRefresh(state = refreshState), - 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, -) { - if (refreshing.value) { - LinearProgressIndicator( - modifier = Modifier - .fillMaxWidth() - .clip(shape = CircleShape) + Loader( + modifier = Modifier.align(Alignment.TopCenter), + refreshState = refreshState, + refreshing = refreshing, ) } - - PullRefreshIndicator( - refreshing = false, - state = refreshState, - ) } @Composable diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/list/LexiconViewModel.kt similarity index 96% rename from app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconViewModel.kt rename to app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/list/LexiconViewModel.kt index c437ac7..cff0d49 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/LexiconViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/list/LexiconViewModel.kt @@ -1,4 +1,4 @@ -package com.pixelized.rplexicon.ui.screens.lexicon +package com.pixelized.rplexicon.ui.screens.lexicon.list import android.util.Log 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.model.Lexicon 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 kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchItem.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/search/SearchItem.kt similarity index 99% rename from app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchItem.kt rename to app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/search/SearchItem.kt index 8dc9746..ed2aac5 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchItem.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/search/SearchItem.kt @@ -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.foundation.layout.Arrangement diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/search/SearchScreen.kt similarity index 98% rename from app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchScreen.kt rename to app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/search/SearchScreen.kt index cc0b956..928c338 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/search/SearchScreen.kt @@ -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_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.TextFieldUio 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.utilitary.composable.stringResource import com.pixelized.rplexicon.utilitary.extentions.lexicon @@ -77,7 +77,7 @@ fun SearchScreen( form = viewModel.form, onItem = { item -> val form = viewModel.form - screen.navigateToCharacterDetail( + screen.navigateToLexiconDetail( id = item.id, highlight = form.search.value.value.takeIf { it.isNotEmpty() }, race = form.race.value.value, diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/search/SearchViewModel.kt similarity index 98% rename from app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchViewModel.kt rename to app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/search/SearchViewModel.kt index 03538d7..d490625 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/search/SearchViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/search/SearchViewModel.kt @@ -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.mutableStateOf diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/detail/QuestDetailScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/detail/QuestDetailScreen.kt new file mode 100644 index 0000000..d755ede --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/detail/QuestDetailScreen.kt @@ -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, +) { + @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, +) { + @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, + 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, +) { + LexiconTheme { + Surface { + QuestDetailContent( + item = preview, + onBack = { }, + ) + } + } +} + +private class QuestDetailPreviewProvider : PreviewParameterProvider> { + override val values: Sequence> = 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.", + ) + ) + ) + ), + ) +} + diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/detail/QuestDetailViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/detail/QuestDetailViewModel.kt new file mode 100644 index 0000000..9383dd9 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/detail/QuestDetailViewModel.kt @@ -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 + + 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, + ) + }, + ) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/list/QuestItem.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/list/QuestItem.kt new file mode 100644 index 0000000..b395f8d --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/list/QuestItem.kt @@ -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 { + override val values: Sequence = sequenceOf( + QuestItemUio.preview(complete = false, placeHolder = false), + QuestItemUio.preview(complete = true, placeHolder = false), + QuestItemUio.preview(complete = true, placeHolder = true), + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/list/QuestListScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/list/QuestListScreen.kt new file mode 100644 index 0000000..6f27605 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/list/QuestListScreen.kt @@ -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, + items: State>, + 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 = { }, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/list/QuestListViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/list/QuestListViewModel.kt new file mode 100644 index 0000000..8b444a3 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/quest/list/QuestListViewModel.kt @@ -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 get() = _isLoading + + private val _items = mutableStateOf>(emptyList()) + val items: State> get() = _items + +// private val _error = MutableSharedFlow() +// val error: SharedFlow 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" + } +} diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/theme/Theme.kt b/app/src/main/java/com/pixelized/rplexicon/ui/theme/Theme.kt index 6d6c808..868e8dc 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/theme/Theme.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/theme/Theme.kt @@ -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.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 { error("LocalLexiconTheme not ready yet.") @@ -29,6 +31,7 @@ data class LexiconTheme( val colorScheme: LexiconColors, val shapes: LexiconShapes, val dimens: LexiconDimens, + val typography: LexiconTypography, ) @Composable @@ -44,6 +47,7 @@ fun LexiconTheme( }, shapes = lexiconShapes(), dimens = lexiconDimen(), + typography = lexiconTypography(), ) } @@ -60,14 +64,16 @@ fun LexiconTheme( } } - CompositionLocalProvider( - LocalLexiconTheme provides lexiconTheme, - ) { - MaterialTheme( - colorScheme = lexiconTheme.colorScheme.base, - shapes = lexiconTheme.shapes.base, - typography = Typography, - content = content - ) - } + MaterialTheme( + colorScheme = lexiconTheme.colorScheme.base, + shapes = lexiconTheme.shapes.base, + typography = lexiconTheme.typography.base, + content = { + CompositionLocalProvider( + LocalLexiconTheme provides lexiconTheme, + ) { + content() + } + } + ) } \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/theme/Type.kt b/app/src/main/java/com/pixelized/rplexicon/ui/theme/Type.kt deleted file mode 100644 index 6b7ad06..0000000 --- a/app/src/main/java/com/pixelized/rplexicon/ui/theme/Type.kt +++ /dev/null @@ -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 - ) - */ -) \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/theme/dimen/LexiconDimens.kt b/app/src/main/java/com/pixelized/rplexicon/ui/theme/dimen/LexiconDimens.kt index 6b0aad6..883cf17 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/theme/dimen/LexiconDimens.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/theme/dimen/LexiconDimens.kt @@ -12,7 +12,7 @@ data class LexiconDimens( ) fun lexiconDimen( - item: Dp = 48.dp + item: Dp = 52.dp ) = LexiconDimens( item = item, ) \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/theme/typography/LexiconTypography.kt b/app/src/main/java/com/pixelized/rplexicon/ui/theme/typography/LexiconTypography.kt new file mode 100644 index 0000000..e789ec9 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/theme/typography/LexiconTypography.kt @@ -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() \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/exceptions/IncompatibleSheetStructure.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/exceptions/IncompatibleSheetStructure.kt new file mode 100644 index 0000000..e6fab1e --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/exceptions/IncompatibleSheetStructure.kt @@ -0,0 +1,3 @@ +package com.pixelized.rplexicon.utilitary.exceptions + +class IncompatibleSheetStructure(message: String?) : Exception(message) \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/exceptions/ServiceNotReady.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/exceptions/ServiceNotReady.kt new file mode 100644 index 0000000..5a7a9f1 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/exceptions/ServiceNotReady.kt @@ -0,0 +1,3 @@ +package com.pixelized.rplexicon.utilitary.exceptions + +class ServiceNotReady : Exception() \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/ModifierEx.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/ModifierEx.kt index 60d8a20..c526fb5 100644 --- a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/ModifierEx.kt +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/ModifierEx.kt @@ -3,12 +3,17 @@ package com.pixelized.rplexicon.utilitary.extentions import androidx.compose.animation.core.FiniteAnimationSpec import androidx.compose.animation.core.Transition 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.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.composed import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.dp import com.google.accompanist.placeholder.PlaceholderHighlight import com.google.accompanist.placeholder.placeholder @@ -20,11 +25,21 @@ fun Modifier.placeholder( placeholderFadeTransitionSpec: @Composable Transition.Segment.() -> FiniteAnimationSpec = { spring() }, contentFadeTransitionSpec: @Composable Transition.Segment.() -> FiniteAnimationSpec = { spring() }, visible: () -> Boolean, -): Modifier = placeholder( - visible = visible(), - color = color, - shape = shape, - highlight = highlight, - placeholderFadeTransitionSpec = placeholderFadeTransitionSpec, - contentFadeTransitionSpec = contentFadeTransitionSpec, -) \ No newline at end of file +): Modifier = composed { + placeholder( + visible = visible(), + color = color, + shape = shape, + highlight = highlight, + placeholderFadeTransitionSpec = placeholderFadeTransitionSpec, + contentFadeTransitionSpec = contentFadeTransitionSpec, + ) +} + +@Composable +fun Modifier.cell() = composed { + Modifier + .fillMaxWidth() + .heightIn(min = MaterialTheme.lexicon.dimens.item) + .padding(horizontal = 16.dp, vertical = 4.dp) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/MutableCollectionEx.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/MutableCollectionEx.kt new file mode 100644 index 0000000..9bac9ae --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/MutableCollectionEx.kt @@ -0,0 +1,8 @@ +package com.pixelized.rplexicon.utilitary.extentions + +fun MutableCollection?.sheet(): List<*>? { + return this?.firstOrNull { + val sheet = it as? ArrayList<*> + sheet != null + } as List<*>? +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/RegexEx.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/RegexEx.kt index 6d52988..dc6fe3a 100644 --- a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/RegexEx.kt +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/RegexEx.kt @@ -20,18 +20,18 @@ fun Regex?.foldAll( } }?.takeIf { it.isNotEmpty() }?.let { "$it..." } -fun Regex.annotatedSpan( +fun Regex?.annotatedSpan( input: String, startIndex: Int = 0, spanStyle: SpanStyle ): List> { - return findAll(input = input, startIndex = startIndex).map { + return this?.findAll(input = input, startIndex = startIndex)?.map { AnnotatedString.Range( item = spanStyle, start = it.range.first, end = it.range.last + 1 ) - }.toList() + }?.toList() ?: emptyList() } fun Regex.annotatedString( diff --git a/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/SheetEx.kt b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/SheetEx.kt new file mode 100644 index 0000000..33df48e --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/utilitary/extentions/SheetEx.kt @@ -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): HashMap { + // 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() + 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 +} \ No newline at end of file diff --git a/app/src/main/res/drawable/art_clip_1.xml b/app/src/main/res/drawable/art_clip_1.xml new file mode 100644 index 0000000..480a2ee --- /dev/null +++ b/app/src/main/res/drawable/art_clip_1.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/art_divider_1.xml b/app/src/main/res/drawable/art_divider_1.xml new file mode 100644 index 0000000..9fbbdc5 --- /dev/null +++ b/app/src/main/res/drawable/art_divider_1.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index d3ab337..7dc617a 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,23 +1,14 @@ - + android:viewportWidth="512" + android:viewportHeight="512"> + - - - + android:fillColor="#FF6650a4" + android:pathData="M248,20.3L72,132.6l176,-3.8L248,20.3zM264,20.3v108.5l175.7,3.8L264,20.3zM307.1,70.27c2.8,0.06 5.8,0.75 9.2,2.08 2.3,0.91 4.1,1.91 5.6,3.07 1.5,1.15 2.8,2.5 3.7,3.79 1.5,2.06 2.2,4.04 2.6,6.25 2.4,-1.77 5.2,-2.98 8.2,-3.84 3.4,-0.73 7.2,-0.35 11.1,1.23 4.6,1.82 8.1,4.19 10.3,7.11 2.2,2.93 3.5,5.97 4,9.13 0.3,1.71 0.3,3.41 0.1,5.01 -0.1,1.7 -0.6,3.3 -1.2,4.7 -0.5,1.5 -1.2,3 -2.3,4.3 -1,1.3 -2.1,2.6 -3.5,3.6 -2.5,2 -5.5,3.3 -9.1,3.9 -3.6,0.6 -7.7,0 -12.3,-1.8 -4,-1.6 -7,-3.8 -9,-6.7 -1.6,-2.6 -2.9,-5.5 -3.4,-8.4 -2,1.5 -4.2,2.5 -6.9,2.9 -3,0.6 -6.5,0 -10.7,-1.6 -2.4,-1 -4.5,-2.1 -6.2,-3.3 -1.8,-1.3 -3.2,-2.61 -4.3,-4.08 -2.1,-2.58 -3.2,-5.37 -3.5,-8.35 -0.2,-2.9 0.2,-5.65 1.1,-8.15 0.5,-1.29 1.3,-2.57 2.1,-3.92 1,-1.1 2,-2.21 3.1,-3.26 2.4,-1.77 5.2,-2.97 8.5,-3.53 0.9,-0.12 1.8,-0.17 2.8,-0.14zM208,75.56c4.8,0.05 10.9,3.57 9,10.04 -4,6.9 -10.3,12.17 -18,14.8 -7.4,2.5 -15,4.4 -22,1.9 -3,-2.3 -13,-9.4 -15,-3.4 -1.2,15.3 1,13 -11,17.8L151,92.3c10,-3.9 21,-4.5 31,1.3 8,4.2 19,1.5 24,-5.8 1,-6.5 -8,-4.5 -12,-3.3 -3,-8.3 7.8,-8.43 13,-8.9 0.3,-0.03 0.6,-0.04 1,-0.04zM308.5,80.02c-0.9,0.01 -1.8,0.14 -2.8,0.36 -2.4,0.61 -4.2,2.17 -5.1,4.67 -1,2.42 -0.8,4.74 0.6,6.88 1.4,2.22 3.3,3.73 6,4.78 2.9,1.15 5.4,1.41 8,0.73 2.5,-0.56 4.3,-2.12 5.2,-4.54 1,-2.5 0.8,-4.82 -0.7,-7.01 -1.4,-2.14 -3.5,-3.77 -6.4,-4.92 -1.6,-0.66 -3.2,-0.96 -4.8,-0.95zM337.4,90.17c-1.1,0.05 -2.2,0.27 -3.2,0.65 -2.7,1.17 -4.5,2.96 -5.4,5.39 -1,2.5 -0.9,5.09 0.4,7.59 1.2,2.6 3.6,4.6 7.2,6.1 2.9,1.1 5.8,1.3 8.6,0.6 2.8,-0.8 4.7,-2.7 5.8,-5.6 1.1,-2.9 1.1,-5.53 -0.5,-8.01 -1.5,-2.47 -3.7,-4.37 -6.6,-5.51 -2.3,-0.9 -4.4,-1.29 -6.3,-1.21zM242,144.9L55,149l72,192.9 115,-197zM270,144.9l115.4,197L456.6,149 270,144.9zM256,152.4L139,352.6h234.1L256,152.4zM372.6,168.8l19.2,42.5 7.2,-3.3 4.1,9.2 -7.1,3.2 6.3,14 -10.4,4.7 -6.3,-14 -30.2,13.6 -3.9,-8.7c1.4,-9.2 4.4,-27.8 8.9,-55.7l1.8,-0.8 0.8,-0.3 3.1,-1.5 6.5,-2.9zM146.7,180.9h1.3c2.9,0 5.5,0.5 7.8,1.6 6.9,3.2 10.7,8.4 11.7,15.3 0.9,6.9 -1,15.3 -5.7,25.1 -4.7,9.7 -10,16.5 -15.8,20.3 -6,3.8 -12.3,4.1 -19.1,1 -5.9,-2.8 -9.4,-6.7 -10.6,-11.9 -1.2,-5.3 -0.9,-9.7 0.9,-13.5l9.6,4.4c-0.9,1.7 -1.1,3.8 -0.8,6.3 0.3,2.6 1.9,4.5 5,6 3.1,1.4 6.1,1.3 9.2,-0.2 3.1,-1.4 6.3,-5.2 9.7,-11.3 0.5,-1 1.1,-2.1 1.7,-3.3 -1.8,1.2 -3.6,2 -5.5,2.6 -3.2,0.9 -6.6,0.5 -10.3,-1.2 -4.3,-2 -7.5,-5.5 -9.5,-10.6 -2.1,-5 -1.5,-10.7 1.6,-17.2l0.1,-0.1c1.1,-2.3 2.4,-4.4 4,-6 1.4,-1.7 3.1,-3.1 4.8,-4.2 3.1,-1.9 6.4,-3 9.9,-3.1zM52,186v173.2l62,-5.7L52,186zM460,186l-61.9,167.5 61.9,5.7L460,186zM368.1,186.6c-1.6,9.7 -3.6,22.5 -6.2,38.2l19.6,-8.8 -8.2,-17.9 -5.2,-11.5zM148.4,190.7c-1.5,0.1 -2.9,0.4 -4.3,1.1 -3,1.4 -5.1,3.5 -6.5,6.5 -1.6,3.4 -2.1,6.5 -1.2,9.6 0.9,3 2.7,5.1 5.4,6.4 2.8,1.3 5.7,1.3 8.5,0s5.1,-3.6 6.8,-7c1.4,-2.9 1.7,-5.9 1,-9 -0.8,-3.1 -2.6,-5.3 -5.4,-6.6 -1.4,-0.7 -2.9,-1 -4.3,-1zM251.6,238.4h15.6v84.2h-15.6v-70.2c-8.8,5.8 -15.3,9.6 -19.4,11.2l-6.3,2.8v-14l6.3,-2.8c4.1,-1.8 10.6,-5.4 19.4,-11.2zM453.3,244.6h0.5c3.6,0.3 5.7,7 4.7,11.1 -0.1,18.6 1.1,39.2 -9.7,55.3 -0.9,1.2 -2.2,1.9 -3.7,2.5 -5.8,-4.1 -3,-11.3 1.2,-15.5 1,7.3 5.5,-2.9 6.6,-5.6 1.3,-3.2 3.6,-17.7 -1,-10.2 0.7,4 -6.8,13.1 -9.3,8.1 -5,-14.4 0,-30.5 7,-43.5 1.3,-1.4 2.5,-2.1 3.7,-2.2zM60,245.5c1,0.1 1,1 2,3.6v61.1c-7,-7 -3,-17.4 -4,-26.4 -1,-7.6 2,-16.3 -1,-23.2 -5,-1.7 -6,-17 -3,-12.7 4,4.8 4,-2.7 6,-2.4zM450.9,256.1c-1,0 -2,1 -2.8,3.7 -1.6,5.9 -3.3,13.4 -0.7,19.3 5.1,-2 5.4,-9.6 6.6,-14.5 1.2,-3.3 -0.9,-8.4 -3.1,-8.5zM75,268.2c4,-0.5 7,7.2 9,10.8 3.28,12.7 4.21,13.9 3,16.8 -5,-3.7 -4.87,-7.4 -5.36,-8.9 -1,-3 -1.64,-5.3 -3.64,-8.4 -3.34,2.8 -3,9.1 -3,13.4 0,-1.6 1,-2.3 4,-0.7 7,12.6 12,29.1 7,43.5l-2,1.1c-11,-5.8 -12,-19.4 -14,-30 -1,-12.3 -1,-24.7 2,-36.7 1,-0.6 2,-0.9 3,-0.9zM433.2,273c4.5,0.3 0.8,35.2 0.8,55l-4.4,6.7v-42.3c-4.6,7.5 -9.1,9.1 -6.1,-0.9 4.9,-13.4 7.9,-18.6 9.7,-18.5zM77,299.2c-4,4.7 -2,12.8 -1,18.4 2,5.5 7,10.2 6,1.6 0,-5.7 1,-11.8 -3,-16.4 0,-0.6 -1,-1.9 -2,-3.6zM143,368.6l113,123.1 112.8,-123.1L143,368.6zM122,368.9l-54,4.9 64,41.1c-2,-2.7 -5,-5.7 -7,-8.8 -5,-6.9 -10,-13.6 -19,-16.6 -9,-6.5 -4,-5.3 3,-2.6 -1,-1.8 -1,-2.6 0,-2.6 2,-0.2 9,4.2 10,6.3l25,31.6 65,41.7 -87,-95zM390.2,368.9l-42.4,46.3c6.4,-3.1 11.3,-8.5 17,-12.4 2.4,-1.4 3.7,-1.9 4.3,-1.9 2.1,0 -5.4,7.1 -7.7,10.3 -9.4,9.8 -16,23 -28.6,29.1l18.9,-24.5c-2.3,1.3 -6,3.2 -8.2,4.1l-40.3,44 74.5,-47.6c5.4,-6.7 1.9,-5.6 -5.7,-0.9l-11.4,6c11.4,-13.7 26.8,-23.6 40,-35.6 3.2,-1.5 9.5,-5.6 11,-5.7 0.8,-0.1 0.2,1 -2.8,4.2l-12.6,16c10,-7.6 0.9,3.9 -4.5,5.5 -0.7,1 -1.4,2 -2.2,2.9l54.5,-34.9 -53.8,-4.9zM231.9,385.6c1.4,0 2.7,0.1 4.1,0.2v43.4h-13v-30c-5,-1.4 -11,1.7 -16,-0.3 -4,-2.9 1,-6.8 5,-5.9 3,-0.1 7,0.2 9,-3.2 3.4,-3.1 7,-4.2 10.9,-4.2zM265,386.3s1,0.1 1,0.2c4,0.8 7,0.3 10,0.4h25.6c1.5,3 0.8,7.8 -3.3,7.9 -3.9,0.5 -7.8,-0.4 -11.7,0.2 -4.7,0.2 -9.6,-1.8 -14.6,0.4 -3,1.7 -4,8.5 1,6.1 4,-1.1 7.3,-1.8 10.8,-0.9 7,1.1 15,2.9 19.1,9.2 2.1,3.1 2.7,7.3 0.7,10.7 -3.6,6.5 -11.6,8.4 -18.3,9.7 -2.4,0.4 -4.7,1.4 -7.3,1.2 -7,-0.6 -15,-1.1 -20,-7.1 -3,-2.5 -3,-7.1 2,-6.7 3,-0.1 8,-0.4 10,3.5 3,3.7 9,3 13,2 3.6,-0.5 7.5,-2.6 7.6,-6.7 0.6,-4.2 -3.1,-7.2 -6.9,-7.8 -5.7,-2.3 -11.7,1.4 -17.7,1.8 -3,1.1 -9,0.5 -9,-4.4 1,-4.2 3,-8.1 3,-12.5 0,-3 2,-7 5,-7.2zM398.5,391.3c-0.2,-0.2 -7,5.8 -9.9,8.1l-15.8,13.1c8.6,-4.4 16.5,-9.6 22.3,-17.4 2.6,-2.6 3.5,-3.7 3.4,-3.8zM151,405.5c3,0 8,4.6 10,7l26,31.1c-8,-2.1 -13,-7.1 -18,-13.7 -6,-7.3 -11,-16.6 -21,-19.6 -9,-5 -5,-6.4 2,-2.2 0,-1.9 0,-2.6 1,-2.6z" /> diff --git a/app/src/main/res/drawable/ic_outline_account_circle_24.xml b/app/src/main/res/drawable/ic_outline_account_circle_24.xml new file mode 100644 index 0000000..cc56447 --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_account_circle_24.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/drawable/ic_outline_map_24.xml b/app/src/main/res/drawable/ic_outline_map_24.xml new file mode 100644 index 0000000..255f0a6 --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_map_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_twenty_faces_one_48.xml b/app/src/main/res/drawable/ic_twenty_faces_one_48.xml new file mode 100644 index 0000000..5310911 --- /dev/null +++ b/app/src/main/res/drawable/ic_twenty_faces_one_48.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/font/regal.ttf b/app/src/main/res/font/regal.ttf new file mode 100644 index 0000000..ad1fa4b Binary files /dev/null and b/app/src/main/res/font/regal.ttf differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp index ad565ad..926ae4f 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp index 3326b65..8d4840b 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp index fa8f8f2..8d205b5 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index b730e70..4997a5e 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index 0e079dd..0eb777f 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index dbd2ded..c084387 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index c9e94f3..03aa23c 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index bad24c1..1b802eb 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index c49e5f1..3f5cde9 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index 0c16336..94ced82 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 42b78d5..7d3e718 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,5 +1,5 @@ - Rp-Lexique + Rp-Compagnon Ah !? y\'a un truc qui foire quelque part. La structure du fichier semble avoir changé et n\'est plus compatible avec cette application. @@ -28,6 +28,9 @@ Se connecter avec + Lexique + Journal de quêtes + Rechercher Détails du personnage @@ -42,4 +45,6 @@ Description : Histoire : Mots clés : + + Détails de quête \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e6e015..f13474c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - Rp-Lexicon + Rp-Companion Oups, it should not be rocket science. The file structure appears to have changed and is no longer compatible with this application @@ -28,6 +28,9 @@ Sign in with + Lexicon + Quest logs + Search Character\'s details @@ -42,4 +45,6 @@ Description: History: Tags: + + Quest details \ No newline at end of file