add map list tab

This commit is contained in:
Thomas Andres Gomez 2023-08-07 12:33:56 +02:00
parent 87a1471efe
commit 57d1273435
17 changed files with 527 additions and 82 deletions

View file

@ -32,6 +32,7 @@ class LocationParser @Inject constructor() {
sheetIndex = index, sheetIndex = index,
name = name, name = name,
uri = uri, uri = uri,
marquees = emptyList(),
) )
} else { } else {
null null

View file

@ -0,0 +1,57 @@
package com.pixelized.rplexicon.ui.composable.error
import android.app.Activity
import android.content.Intent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import com.pixelized.rplexicon.LocalSnack
import com.pixelized.rplexicon.R
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
@Stable
sealed class FetchErrorUio {
@Stable
data class Permission(val intent: Intent) : FetchErrorUio()
@Stable
object Structure : FetchErrorUio()
@Stable
object Default : FetchErrorUio()
}
@Composable
fun HandleFetchError(
errors: SharedFlow<FetchErrorUio>,
onPermissionGranted: suspend () -> Unit,
) {
val context = LocalContext.current
val snack = LocalSnack.current
val scope = rememberCoroutineScope()
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult(),
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
scope.launch {
onPermissionGranted()
}
}
}
LaunchedEffect(key1 = "LexiconErrorManagement") {
errors.collect { error ->
when (error) {
is FetchErrorUio.Permission -> launcher.launch(error.intent)
is FetchErrorUio.Structure -> snack.showSnackbar(message = context.getString(R.string.error_structure))
is FetchErrorUio.Default -> snack.showSnackbar(message = context.getString(R.string.error_generic))
}
}
}
}

View file

@ -1,7 +1,6 @@
package com.pixelized.rplexicon.ui.navigation package com.pixelized.rplexicon.ui.navigation
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material3.BottomAppBar import androidx.compose.material3.BottomAppBar
@ -33,8 +32,10 @@ import com.pixelized.rplexicon.LocalSnack
import com.pixelized.rplexicon.R import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.navigation.pages.LEXICON_ROUTE import com.pixelized.rplexicon.ui.navigation.pages.LEXICON_ROUTE
import com.pixelized.rplexicon.ui.navigation.pages.composableLexicon import com.pixelized.rplexicon.ui.navigation.pages.composableLexicon
import com.pixelized.rplexicon.ui.navigation.pages.composableQuestList import com.pixelized.rplexicon.ui.navigation.pages.composableLocations
import com.pixelized.rplexicon.ui.navigation.pages.composableQuests
import com.pixelized.rplexicon.ui.navigation.pages.navigateToLexicon import com.pixelized.rplexicon.ui.navigation.pages.navigateToLexicon
import com.pixelized.rplexicon.ui.navigation.pages.navigateToLocation
import com.pixelized.rplexicon.ui.navigation.pages.navigateToQuestList import com.pixelized.rplexicon.ui.navigation.pages.navigateToQuestList
val LocalPageNavHost = staticCompositionLocalOf<NavHostController> { val LocalPageNavHost = staticCompositionLocalOf<NavHostController> {
@ -44,9 +45,11 @@ val LocalPageNavHost = staticCompositionLocalOf<NavHostController> {
@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class) @OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class)
@Composable @Composable
fun HomeNavHost( fun HomeNavHost(
lexiconListState: LazyListState,
navHostController: NavHostController = rememberAnimatedNavController(), navHostController: NavHostController = rememberAnimatedNavController(),
bottomBarItems: List<BottomBarItem> = rememberBottomBarItems(navHostController = navHostController), bottomBarItems: List<BottomBarItem> = rememberBottomBarItems(navHostController),
lexiconListState: LazyListState,
questListState: LazyListState,
locationListState: LazyListState,
startDestination: String = LEXICON_ROUTE startDestination: String = LEXICON_ROUTE
) { ) {
CompositionLocalProvider( CompositionLocalProvider(
@ -73,7 +76,8 @@ fun HomeNavHost(
tonalElevation = 0.dp, tonalElevation = 0.dp,
) { ) {
bottomBarItems.forEachIndexed { index, item -> bottomBarItems.forEachIndexed { index, item ->
val selectedItem = remember { derivedStateOf { selectedIndex.value == index } } val selectedItem =
remember { derivedStateOf { selectedIndex.value == index } }
NavigationBarItem( NavigationBarItem(
selected = selectedItem.value, selected = selectedItem.value,
onClick = { onClick = {
@ -103,7 +107,8 @@ fun HomeNavHost(
startDestination = startDestination, startDestination = startDestination,
) { ) {
composableLexicon(lazyListState = lexiconListState) composableLexicon(lazyListState = lexiconListState)
composableQuestList() composableQuests(lazyListState = questListState)
composableLocations(lazyListState = locationListState)
} }
} }
} }
@ -124,17 +129,23 @@ private fun rememberBottomBarItems(
navHostController: NavHostController, navHostController: NavHostController,
): List<BottomBarItem> { ): List<BottomBarItem> {
return remember(navHostController) { return remember(navHostController) {
val option = navHostController.pageOption()
listOf( listOf(
BottomBarItem( BottomBarItem(
icon = R.drawable.ic_outline_account_circle_24, icon = R.drawable.ic_outline_account_circle_24,
label = R.string.home_lexicon, label = R.string.home_lexicon,
onClick = { navHostController.navigateToLexicon(navHostController.pageOption()) } onClick = { navHostController.navigateToLexicon(option) }
),
BottomBarItem(
icon = R.drawable.ic_outline_scroll_24,
label = R.string.home_quest_log,
onClick = { navHostController.navigateToQuestList(option) }
), ),
BottomBarItem( BottomBarItem(
icon = R.drawable.ic_outline_map_24, icon = R.drawable.ic_outline_map_24,
label = R.string.home_quest_log, label = R.string.home_location,
onClick = { navHostController.navigateToQuestList(navHostController.pageOption()) } onClick = { navHostController.navigateToLocation(option) }
), )
) )
} }
} }

View file

@ -28,7 +28,9 @@ fun ScreenNavHost(
navHostController: NavHostController = rememberAnimatedNavController(), navHostController: NavHostController = rememberAnimatedNavController(),
startDestination: String = AUTHENTICATION_ROUTE, startDestination: String = AUTHENTICATION_ROUTE,
) { ) {
val lexiconListState: LazyListState = rememberLazyListState() val lexiconListState =rememberLazyListState()
val questListState = rememberLazyListState()
val locationListState = rememberLazyListState()
CompositionLocalProvider( CompositionLocalProvider(
LocalScreenNavHost provides navHostController, LocalScreenNavHost provides navHostController,
@ -38,7 +40,11 @@ fun ScreenNavHost(
startDestination = startDestination, startDestination = startDestination,
) { ) {
composableAuthentication() composableAuthentication()
composableHome(lexiconListState) composableHome(
lexiconListState = lexiconListState,
questListState = questListState,
locationListState = locationListState
)
composableLexiconDetail() composableLexiconDetail()
composableLexiconSearch() composableLexiconSearch()

View file

@ -0,0 +1,32 @@
package com.pixelized.rplexicon.ui.navigation.pages
import androidx.compose.foundation.lazy.LazyListState
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptionsBuilder
import com.pixelized.rplexicon.ui.navigation.NavigationAnimation
import com.pixelized.rplexicon.ui.navigation.animatedComposable
import com.pixelized.rplexicon.ui.screens.location.list.LocationScreen
private const val ROUTE = "locations"
const val LOCATION_ROUTE = ROUTE
fun NavGraphBuilder.composableLocations(
lazyListState: LazyListState,
) {
animatedComposable(
route = LOCATION_ROUTE,
animation = NavigationAnimation.NONE,
) {
LocationScreen(
lazyListState = lazyListState,
)
}
}
fun NavController.navigateToLocation(
option: NavOptionsBuilder.() -> Unit = {},
) {
navigate(route = ROUTE, builder = option)
}

View file

@ -1,5 +1,6 @@
package com.pixelized.rplexicon.ui.navigation.pages package com.pixelized.rplexicon.ui.navigation.pages
import androidx.compose.foundation.lazy.LazyListState
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.NavOptionsBuilder import androidx.navigation.NavOptionsBuilder
@ -11,12 +12,14 @@ private const val ROUTE = "quests"
const val QUEST_LIST_ROUTE = ROUTE const val QUEST_LIST_ROUTE = ROUTE
fun NavGraphBuilder.composableQuestList() { fun NavGraphBuilder.composableQuests(
lazyListState: LazyListState,
) {
animatedComposable( animatedComposable(
route = QUEST_LIST_ROUTE, route = QUEST_LIST_ROUTE,
animation = NavigationAnimation.NONE, animation = NavigationAnimation.NONE,
) { ) {
QuestListScreen() QuestListScreen(lazyListState = lazyListState)
} }
} }

View file

@ -14,12 +14,18 @@ const val HOME_ROUTE = ROUTE
fun NavGraphBuilder.composableHome( fun NavGraphBuilder.composableHome(
lexiconListState: LazyListState, lexiconListState: LazyListState,
questListState: LazyListState,
locationListState: LazyListState,
) { ) {
animatedComposable( animatedComposable(
route = HOME_ROUTE, route = HOME_ROUTE,
animation = NavigationAnimation.Fade, animation = NavigationAnimation.Fade,
) { ) {
HomeNavHost(lexiconListState = lexiconListState) HomeNavHost(
lexiconListState = lexiconListState,
questListState = questListState,
locationListState = locationListState
)
} }
} }

View file

@ -1,11 +1,7 @@
package com.pixelized.rplexicon.ui.screens.lexicon.list package com.pixelized.rplexicon.ui.screens.lexicon.list
import android.app.Activity
import android.content.Intent
import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
@ -29,8 +25,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -38,39 +32,23 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.LocalSnack
import com.pixelized.rplexicon.R import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.composable.FloatingActionButton import com.pixelized.rplexicon.ui.composable.FloatingActionButton
import com.pixelized.rplexicon.ui.composable.Loader import com.pixelized.rplexicon.ui.composable.Loader
import com.pixelized.rplexicon.ui.composable.error.HandleFetchError
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.navigation.screens.navigateToLexiconDetail import com.pixelized.rplexicon.ui.navigation.screens.navigateToLexiconDetail
import com.pixelized.rplexicon.ui.navigation.screens.navigateToLexiconSearch 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.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.cell import com.pixelized.rplexicon.utilitary.extentions.cell
import com.pixelized.rplexicon.utilitary.extentions.lexicon import com.pixelized.rplexicon.utilitary.extentions.lexicon
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Stable
sealed class LexiconErrorUio {
@Stable
data class Permission(val intent: Intent) : LexiconErrorUio()
@Stable
object Structure : LexiconErrorUio()
@Stable
object Default : LexiconErrorUio()
}
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@ -111,9 +89,9 @@ fun LexiconScreen(
}, },
) )
HandleError( HandleFetchError(
errors = viewModel.error, errors = viewModel.error,
onLexiconPermissionGranted = { onPermissionGranted = {
viewModel.fetchLexicon() viewModel.fetchLexicon()
} }
) )
@ -216,36 +194,6 @@ private fun LexiconScreenContent(
} }
} }
@Composable
fun HandleError(
errors: SharedFlow<LexiconErrorUio>,
onLexiconPermissionGranted: suspend () -> Unit,
) {
val context = LocalContext.current
val snack = LocalSnack.current
val scope = rememberCoroutineScope()
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult(),
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
scope.launch {
onLexiconPermissionGranted()
}
}
}
LaunchedEffect(key1 = "LexiconErrorManagement") {
errors.collect { error ->
when (error) {
is Permission -> launcher.launch(error.intent)
is Structure -> snack.showSnackbar(message = context.getString(R.string.error_structure))
is Default -> snack.showSnackbar(message = context.getString(R.string.error_generic))
}
}
}
}
@OptIn(ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
@Preview(uiMode = UI_MODE_NIGHT_NO) @Preview(uiMode = UI_MODE_NIGHT_NO)

View file

@ -9,6 +9,7 @@ import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecovera
import com.pixelized.rplexicon.R import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.model.Lexicon import com.pixelized.rplexicon.model.Lexicon
import com.pixelized.rplexicon.repository.LexiconRepository import com.pixelized.rplexicon.repository.LexiconRepository
import com.pixelized.rplexicon.ui.composable.error.FetchErrorUio
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -27,8 +28,8 @@ class LexiconViewModel @Inject constructor(
private val _items = mutableStateOf<List<LexiconItemUio>>(emptyList()) private val _items = mutableStateOf<List<LexiconItemUio>>(emptyList())
val items: State<List<LexiconItemUio>> get() = _items val items: State<List<LexiconItemUio>> get() = _items
private val _error = MutableSharedFlow<LexiconErrorUio>() private val _error = MutableSharedFlow<FetchErrorUio>()
val error: SharedFlow<LexiconErrorUio> get() = _error val error: SharedFlow<FetchErrorUio> get() = _error
init { init {
viewModelScope.launch { viewModelScope.launch {
@ -77,15 +78,15 @@ class LexiconViewModel @Inject constructor(
// user need to accept OAuth2 permission. // user need to accept OAuth2 permission.
catch (exception: UserRecoverableAuthIOException) { catch (exception: UserRecoverableAuthIOException) {
Log.e(TAG, exception.message, exception) Log.e(TAG, exception.message, exception)
_error.emit(LexiconErrorUio.Permission(intent = exception.intent)) _error.emit(FetchErrorUio.Permission(intent = exception.intent))
} catch (exception: IncompatibleSheetStructure) { } catch (exception: IncompatibleSheetStructure) {
Log.e(TAG, exception.message, exception) Log.e(TAG, exception.message, exception)
_error.emit(LexiconErrorUio.Structure) _error.emit(FetchErrorUio.Structure)
} }
// default exception // default exception
catch (exception: Exception) { catch (exception: Exception) {
Log.e(TAG, exception.message, exception) Log.e(TAG, exception.message, exception)
_error.emit(LexiconErrorUio.Default) _error.emit(FetchErrorUio.Default)
} }
// clean the laoding state // clean the laoding state
finally { finally {

View file

@ -0,0 +1,114 @@
package com.pixelized.rplexicon.ui.screens.location.list
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.LOS_HOLLOW
import com.pixelized.rplexicon.utilitary.extentions.cell
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import com.pixelized.rplexicon.utilitary.extentions.placeholder
@Stable
data class LocationItemUio(
val id: Int,
val title: String,
val placeholder: Boolean = false,
) {
companion object {
fun preview(
id: Int = 0,
title: String = "Daggerfall",
placeHolder: Boolean = false,
): LocationItemUio {
return LocationItemUio(
id = id,
title = title,
placeholder = placeHolder,
)
}
}
}
@Composable
fun LocationItem(
modifier: Modifier = Modifier,
item: LocationItemUio,
) {
val typography = MaterialTheme.lexicon.typography
Box(
modifier = modifier,
contentAlignment = Alignment.CenterStart,
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
modifier = when (item.placeholder) {
true -> Modifier.placeholder { true }
else -> Modifier.alignByBaseline()
},
text = LOS_HOLLOW,
)
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: LocationItemUio,
) {
LexiconTheme {
Surface {
LocationItem(
modifier = Modifier.cell(),
item = preview,
)
}
}
}
private class QuestItemPreviewProvider : PreviewParameterProvider<LocationItemUio> {
override val values: Sequence<LocationItemUio> = sequenceOf(
LocationItemUio.preview(placeHolder = false),
LocationItemUio.preview(placeHolder = true),
)
}

View file

@ -0,0 +1,167 @@
package com.pixelized.rplexicon.ui.screens.location.list
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
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.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.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.ui.composable.Loader
import com.pixelized.rplexicon.ui.composable.error.HandleFetchError
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
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 LocationScreen(
viewModel: LocationViewModel = hiltViewModel(),
lazyListState: LazyListState,
) {
val scope = rememberCoroutineScope()
val screen = LocalScreenNavHost.current
val refresh = rememberPullRefreshState(
refreshing = false,
onRefresh = {
scope.launch {
viewModel.fetchLocation()
}
},
)
Surface {
LocationContent(
items = viewModel.items,
lazyColumnState = lazyListState,
refreshState = refresh,
refreshing = viewModel.isLoading,
onItem = {
// screen.navigateToQuestDetail(id = it.id)
},
)
HandleFetchError(
errors = viewModel.error,
onPermissionGranted = {
viewModel.fetchLocation()
}
)
}
}
@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterialApi::class)
@Composable
private fun LocationContent(
modifier: Modifier = Modifier,
lazyColumnState: LazyListState,
refreshState: PullRefreshState,
refreshing: State<Boolean>,
items: State<List<LocationItemUio>>,
onItem: (LocationItemUio) -> Unit,
) {
Box(
modifier = modifier,
contentAlignment = Alignment.TopCenter,
) {
AnimatedContent(
targetState = items.value.isEmpty(),
transitionSpec = MaterialTheme.lexicon.animation.itemList,
label = "AnimatedLocations"
) { empty ->
when (empty) {
true -> LazyColumn(
modifier = Modifier
.fillMaxSize()
.pullRefresh(state = refreshState),
state = lazyColumnState,
contentPadding = MaterialTheme.lexicon.dimens.itemListPadding,
) {
items(count = 2) {
LocationItem(
modifier = Modifier.cell(),
item = LocationItemUio.preview(placeHolder = true),
)
}
}
else -> LazyColumn(
modifier = Modifier
.fillMaxSize()
.pullRefresh(state = refreshState),
state = lazyColumnState,
contentPadding = MaterialTheme.lexicon.dimens.itemListPadding,
) {
items(
items = items.value,
key = { it.id },
contentType = { "Location" },
) {
LocationItem(
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 {
LocationContent(
modifier = Modifier.fillMaxSize(),
lazyColumnState = rememberLazyListState(),
refreshState = rememberPullRefreshState(
refreshing = false,
onRefresh = {},
),
refreshing = remember { mutableStateOf(false) },
items = remember {
mutableStateOf(
listOf(
LocationItemUio.preview(id = 0, title = "Daggerfall"),
LocationItemUio.preview(id = 1, title = "Athkatla"),
)
)
},
onItem = { },
)
}
}
}

View file

@ -0,0 +1,77 @@
package com.pixelized.rplexicon.ui.screens.location.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.LocationRepository
import com.pixelized.rplexicon.ui.composable.error.FetchErrorUio
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class LocationViewModel @Inject constructor(
private val repository: LocationRepository,
) : ViewModel() {
private val _isLoading = mutableStateOf(false)
val isLoading: State<Boolean> get() = _isLoading
private val _items = mutableStateOf<List<LocationItemUio>>(emptyList())
val items: State<List<LocationItemUio>> get() = _items
private val _error = MutableSharedFlow<FetchErrorUio>()
val error: SharedFlow<FetchErrorUio> get() = _error
init {
viewModelScope.launch {
launch {
repository.data.collect { items ->
_items.value = items.map { item ->
LocationItemUio(
id = item.id,
title = item.name,
)
}
}
}
launch {
fetchLocation()
}
}
}
suspend fun fetchLocation() {
try {
_isLoading.value = true
repository.fetchLocation()
}
// user need to accept OAuth2 permission.
catch (exception: UserRecoverableAuthIOException) {
Log.e(TAG, exception.message, exception)
_error.emit(FetchErrorUio.Permission(intent = exception.intent))
} catch (exception: IncompatibleSheetStructure) {
Log.e(TAG, exception.message, exception)
_error.emit(FetchErrorUio.Structure)
}
// default exception
catch (exception: Exception) {
Log.e(TAG, exception.message, exception)
_error.emit(FetchErrorUio.Default)
}
// clean the laoding state
finally {
_isLoading.value = false
}
}
companion object {
private const val TAG = "LocationViewModel"
}
}

View file

@ -26,6 +26,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.ui.composable.Loader import com.pixelized.rplexicon.ui.composable.Loader
import com.pixelized.rplexicon.ui.composable.error.HandleFetchError
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.navigation.screens.navigateToQuestDetail import com.pixelized.rplexicon.ui.navigation.screens.navigateToQuestDetail
import com.pixelized.rplexicon.ui.theme.LexiconTheme import com.pixelized.rplexicon.ui.theme.LexiconTheme
@ -38,9 +39,9 @@ import kotlinx.coroutines.launch
@Composable @Composable
fun QuestListScreen( fun QuestListScreen(
viewModel: QuestListViewModel = hiltViewModel(), viewModel: QuestListViewModel = hiltViewModel(),
lazyListState: LazyListState,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val lazyListState = rememberLazyListState()
val screen = LocalScreenNavHost.current val screen = LocalScreenNavHost.current
val refresh = rememberPullRefreshState( val refresh = rememberPullRefreshState(
@ -62,6 +63,13 @@ fun QuestListScreen(
screen.navigateToQuestDetail(id = it.id) screen.navigateToQuestDetail(id = it.id)
}, },
) )
HandleFetchError(
errors = viewModel.error,
onPermissionGranted = {
viewModel.fetchQuests()
}
)
} }
} }
@ -110,7 +118,7 @@ private fun QuestListContent(
items( items(
items = items.value, items = items.value,
key = { it.id }, key = { it.id },
contentType = { "Lexicon" }, contentType = { "Quest" },
) { ) {
QuestItem( QuestItem(
modifier = Modifier modifier = Modifier

View file

@ -7,8 +7,11 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException
import com.pixelized.rplexicon.repository.QuestRepository import com.pixelized.rplexicon.repository.QuestRepository
import com.pixelized.rplexicon.ui.composable.error.FetchErrorUio
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ -23,8 +26,8 @@ class QuestListViewModel @Inject constructor(
private val _items = mutableStateOf<List<QuestItemUio>>(emptyList()) private val _items = mutableStateOf<List<QuestItemUio>>(emptyList())
val items: State<List<QuestItemUio>> get() = _items val items: State<List<QuestItemUio>> get() = _items
// private val _error = MutableSharedFlow<LexiconErrorUio>() private val _error = MutableSharedFlow<FetchErrorUio>()
// val error: SharedFlow<LexiconErrorUio> get() = _error val error: SharedFlow<FetchErrorUio> get() = _error
init { init {
viewModelScope.launch { viewModelScope.launch {
@ -53,15 +56,15 @@ class QuestListViewModel @Inject constructor(
// user need to accept OAuth2 permission. // user need to accept OAuth2 permission.
catch (exception: UserRecoverableAuthIOException) { catch (exception: UserRecoverableAuthIOException) {
Log.e(TAG, exception.message, exception) Log.e(TAG, exception.message, exception)
// _error.emit(LexiconErrorUio.Permission(intent = exception.intent)) _error.emit(FetchErrorUio.Permission(intent = exception.intent))
} catch (exception: IncompatibleSheetStructure) { } catch (exception: IncompatibleSheetStructure) {
Log.e(TAG, exception.message, exception) Log.e(TAG, exception.message, exception)
// _error.emit(LexiconErrorUio.Structure) _error.emit(FetchErrorUio.Structure)
} }
// default exception // default exception
catch (exception: Exception) { catch (exception: Exception) {
Log.e(TAG, exception.message, exception) Log.e(TAG, exception.message, exception)
// _error.emit(LexiconErrorUio.Default) _error.emit(FetchErrorUio.Default)
} }
// clean the laoding state // clean the laoding state
finally { finally {

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M8,2C6.355,2 5,3.355 5,5L5,16L2,16L2,19C2,20.645 3.355,22 5,22L14,22L15,22C16.645,22 18,20.645 18,19L18,8L22,8L22,5C22,3.355 20.645,2 19,2L8,2zM8,4L16.188,4C16.074,4.315 16,4.648 16,5L16,19C16,19.56 15.572,19.991 15.014,19.998L15.014,19.986C14.45,19.994 14.008,19.563 14,19L13.973,16.004L13,16.014L13,16L7,16L7,5C7,4.435 7.435,4 8,4zM19,4C19.565,4 20,4.435 20,5L20,6L18,6L18,5C18,4.435 18.435,4 19,4zM4,18L11.99,18L12,19.025L12,19.027C12.005,19.37 12.081,19.694 12.193,20L5,20C4.435,20 4,19.565 4,19L4,18z"/>
</vector>

View file

@ -30,6 +30,7 @@
<string name="home_lexicon">Lexique</string> <string name="home_lexicon">Lexique</string>
<string name="home_quest_log">Journal de quêtes</string> <string name="home_quest_log">Journal de quêtes</string>
<string name="home_location">Cartes</string>
<string name="lexicon_search">Rechercher</string> <string name="lexicon_search">Rechercher</string>

View file

@ -30,6 +30,7 @@
<string name="home_lexicon">Lexicon</string> <string name="home_lexicon">Lexicon</string>
<string name="home_quest_log">Quest logs</string> <string name="home_quest_log">Quest logs</string>
<string name="home_location">Location</string>
<string name="lexicon_search">Search</string> <string name="lexicon_search">Search</string>