add map list tab
This commit is contained in:
parent
87a1471efe
commit
57d1273435
17 changed files with 527 additions and 82 deletions
|
|
@ -32,6 +32,7 @@ class LocationParser @Inject constructor() {
|
|||
sheetIndex = index,
|
||||
name = name,
|
||||
uri = uri,
|
||||
marquees = emptyList(),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package com.pixelized.rplexicon.ui.navigation
|
||||
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
|
|
@ -33,8 +32,10 @@ import com.pixelized.rplexicon.LocalSnack
|
|||
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.composableLocations
|
||||
import com.pixelized.rplexicon.ui.navigation.pages.composableQuests
|
||||
import com.pixelized.rplexicon.ui.navigation.pages.navigateToLexicon
|
||||
import com.pixelized.rplexicon.ui.navigation.pages.navigateToLocation
|
||||
import com.pixelized.rplexicon.ui.navigation.pages.navigateToQuestList
|
||||
|
||||
val LocalPageNavHost = staticCompositionLocalOf<NavHostController> {
|
||||
|
|
@ -44,9 +45,11 @@ val LocalPageNavHost = staticCompositionLocalOf<NavHostController> {
|
|||
@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun HomeNavHost(
|
||||
lexiconListState: LazyListState,
|
||||
navHostController: NavHostController = rememberAnimatedNavController(),
|
||||
bottomBarItems: List<BottomBarItem> = rememberBottomBarItems(navHostController = navHostController),
|
||||
bottomBarItems: List<BottomBarItem> = rememberBottomBarItems(navHostController),
|
||||
lexiconListState: LazyListState,
|
||||
questListState: LazyListState,
|
||||
locationListState: LazyListState,
|
||||
startDestination: String = LEXICON_ROUTE
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
|
|
@ -73,7 +76,8 @@ fun HomeNavHost(
|
|||
tonalElevation = 0.dp,
|
||||
) {
|
||||
bottomBarItems.forEachIndexed { index, item ->
|
||||
val selectedItem = remember { derivedStateOf { selectedIndex.value == index } }
|
||||
val selectedItem =
|
||||
remember { derivedStateOf { selectedIndex.value == index } }
|
||||
NavigationBarItem(
|
||||
selected = selectedItem.value,
|
||||
onClick = {
|
||||
|
|
@ -103,7 +107,8 @@ fun HomeNavHost(
|
|||
startDestination = startDestination,
|
||||
) {
|
||||
composableLexicon(lazyListState = lexiconListState)
|
||||
composableQuestList()
|
||||
composableQuests(lazyListState = questListState)
|
||||
composableLocations(lazyListState = locationListState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -124,17 +129,23 @@ private fun rememberBottomBarItems(
|
|||
navHostController: NavHostController,
|
||||
): List<BottomBarItem> {
|
||||
return remember(navHostController) {
|
||||
val option = navHostController.pageOption()
|
||||
listOf(
|
||||
BottomBarItem(
|
||||
icon = R.drawable.ic_outline_account_circle_24,
|
||||
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(
|
||||
icon = R.drawable.ic_outline_map_24,
|
||||
label = R.string.home_quest_log,
|
||||
onClick = { navHostController.navigateToQuestList(navHostController.pageOption()) }
|
||||
),
|
||||
label = R.string.home_location,
|
||||
onClick = { navHostController.navigateToLocation(option) }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -28,7 +28,9 @@ fun ScreenNavHost(
|
|||
navHostController: NavHostController = rememberAnimatedNavController(),
|
||||
startDestination: String = AUTHENTICATION_ROUTE,
|
||||
) {
|
||||
val lexiconListState: LazyListState = rememberLazyListState()
|
||||
val lexiconListState =rememberLazyListState()
|
||||
val questListState = rememberLazyListState()
|
||||
val locationListState = rememberLazyListState()
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalScreenNavHost provides navHostController,
|
||||
|
|
@ -38,7 +40,11 @@ fun ScreenNavHost(
|
|||
startDestination = startDestination,
|
||||
) {
|
||||
composableAuthentication()
|
||||
composableHome(lexiconListState)
|
||||
composableHome(
|
||||
lexiconListState = lexiconListState,
|
||||
questListState = questListState,
|
||||
locationListState = locationListState
|
||||
)
|
||||
|
||||
composableLexiconDetail()
|
||||
composableLexiconSearch()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.pixelized.rplexicon.ui.navigation.pages
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.NavOptionsBuilder
|
||||
|
|
@ -11,12 +12,14 @@ private const val ROUTE = "quests"
|
|||
|
||||
const val QUEST_LIST_ROUTE = ROUTE
|
||||
|
||||
fun NavGraphBuilder.composableQuestList() {
|
||||
fun NavGraphBuilder.composableQuests(
|
||||
lazyListState: LazyListState,
|
||||
) {
|
||||
animatedComposable(
|
||||
route = QUEST_LIST_ROUTE,
|
||||
animation = NavigationAnimation.NONE,
|
||||
) {
|
||||
QuestListScreen()
|
||||
QuestListScreen(lazyListState = lazyListState)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,12 +14,18 @@ const val HOME_ROUTE = ROUTE
|
|||
|
||||
fun NavGraphBuilder.composableHome(
|
||||
lexiconListState: LazyListState,
|
||||
questListState: LazyListState,
|
||||
locationListState: LazyListState,
|
||||
) {
|
||||
animatedComposable(
|
||||
route = HOME_ROUTE,
|
||||
animation = NavigationAnimation.Fade,
|
||||
) {
|
||||
HomeNavHost(lexiconListState = lexiconListState)
|
||||
HomeNavHost(
|
||||
lexiconListState = lexiconListState,
|
||||
questListState = questListState,
|
||||
locationListState = locationListState
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
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_YES
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
|
|
@ -29,8 +25,6 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
|
@ -38,39 +32,23 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.R
|
||||
import com.pixelized.rplexicon.ui.composable.FloatingActionButton
|
||||
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.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.cell
|
||||
import com.pixelized.rplexicon.utilitary.extentions.lexicon
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
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)
|
||||
@Composable
|
||||
|
|
@ -111,9 +89,9 @@ fun LexiconScreen(
|
|||
},
|
||||
)
|
||||
|
||||
HandleError(
|
||||
HandleFetchError(
|
||||
errors = viewModel.error,
|
||||
onLexiconPermissionGranted = {
|
||||
onPermissionGranted = {
|
||||
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)
|
||||
@Composable
|
||||
@Preview(uiMode = UI_MODE_NIGHT_NO)
|
||||
|
|
|
|||
|
|
@ -9,6 +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.ui.composable.error.FetchErrorUio
|
||||
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
|
@ -27,8 +28,8 @@ class LexiconViewModel @Inject constructor(
|
|||
private val _items = mutableStateOf<List<LexiconItemUio>>(emptyList())
|
||||
val items: State<List<LexiconItemUio>> get() = _items
|
||||
|
||||
private val _error = MutableSharedFlow<LexiconErrorUio>()
|
||||
val error: SharedFlow<LexiconErrorUio> get() = _error
|
||||
private val _error = MutableSharedFlow<FetchErrorUio>()
|
||||
val error: SharedFlow<FetchErrorUio> get() = _error
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
|
|
@ -77,15 +78,15 @@ class LexiconViewModel @Inject constructor(
|
|||
// user need to accept OAuth2 permission.
|
||||
catch (exception: UserRecoverableAuthIOException) {
|
||||
Log.e(TAG, exception.message, exception)
|
||||
_error.emit(LexiconErrorUio.Permission(intent = exception.intent))
|
||||
_error.emit(FetchErrorUio.Permission(intent = exception.intent))
|
||||
} catch (exception: IncompatibleSheetStructure) {
|
||||
Log.e(TAG, exception.message, exception)
|
||||
_error.emit(LexiconErrorUio.Structure)
|
||||
_error.emit(FetchErrorUio.Structure)
|
||||
}
|
||||
// default exception
|
||||
catch (exception: Exception) {
|
||||
Log.e(TAG, exception.message, exception)
|
||||
_error.emit(LexiconErrorUio.Default)
|
||||
_error.emit(FetchErrorUio.Default)
|
||||
}
|
||||
// clean the laoding state
|
||||
finally {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
|
@ -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 = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ 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.navigation.screens.navigateToQuestDetail
|
||||
import com.pixelized.rplexicon.ui.theme.LexiconTheme
|
||||
|
|
@ -38,9 +39,9 @@ import kotlinx.coroutines.launch
|
|||
@Composable
|
||||
fun QuestListScreen(
|
||||
viewModel: QuestListViewModel = hiltViewModel(),
|
||||
lazyListState: LazyListState,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val lazyListState = rememberLazyListState()
|
||||
val screen = LocalScreenNavHost.current
|
||||
|
||||
val refresh = rememberPullRefreshState(
|
||||
|
|
@ -62,6 +63,13 @@ fun QuestListScreen(
|
|||
screen.navigateToQuestDetail(id = it.id)
|
||||
},
|
||||
)
|
||||
|
||||
HandleFetchError(
|
||||
errors = viewModel.error,
|
||||
onPermissionGranted = {
|
||||
viewModel.fetchQuests()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -110,7 +118,7 @@ private fun QuestListContent(
|
|||
items(
|
||||
items = items.value,
|
||||
key = { it.id },
|
||||
contentType = { "Lexicon" },
|
||||
contentType = { "Quest" },
|
||||
) {
|
||||
QuestItem(
|
||||
modifier = Modifier
|
||||
|
|
|
|||
|
|
@ -7,8 +7,11 @@ 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.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
|
||||
|
||||
|
|
@ -23,8 +26,8 @@ class QuestListViewModel @Inject constructor(
|
|||
private val _items = mutableStateOf<List<QuestItemUio>>(emptyList())
|
||||
val items: State<List<QuestItemUio>> get() = _items
|
||||
|
||||
// private val _error = MutableSharedFlow<LexiconErrorUio>()
|
||||
// val error: SharedFlow<LexiconErrorUio> get() = _error
|
||||
private val _error = MutableSharedFlow<FetchErrorUio>()
|
||||
val error: SharedFlow<FetchErrorUio> get() = _error
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
|
|
@ -53,15 +56,15 @@ class QuestListViewModel @Inject constructor(
|
|||
// user need to accept OAuth2 permission.
|
||||
catch (exception: UserRecoverableAuthIOException) {
|
||||
Log.e(TAG, exception.message, exception)
|
||||
// _error.emit(LexiconErrorUio.Permission(intent = exception.intent))
|
||||
_error.emit(FetchErrorUio.Permission(intent = exception.intent))
|
||||
} catch (exception: IncompatibleSheetStructure) {
|
||||
Log.e(TAG, exception.message, exception)
|
||||
// _error.emit(LexiconErrorUio.Structure)
|
||||
_error.emit(FetchErrorUio.Structure)
|
||||
}
|
||||
// default exception
|
||||
catch (exception: Exception) {
|
||||
Log.e(TAG, exception.message, exception)
|
||||
// _error.emit(LexiconErrorUio.Default)
|
||||
_error.emit(FetchErrorUio.Default)
|
||||
}
|
||||
// clean the laoding state
|
||||
finally {
|
||||
|
|
|
|||
9
app/src/main/res/drawable/ic_outline_scroll_24.xml
Normal file
9
app/src/main/res/drawable/ic_outline_scroll_24.xml
Normal 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>
|
||||
|
|
@ -30,6 +30,7 @@
|
|||
|
||||
<string name="home_lexicon">Lexique</string>
|
||||
<string name="home_quest_log">Journal de quêtes</string>
|
||||
<string name="home_location">Cartes</string>
|
||||
|
||||
<string name="lexicon_search">Rechercher</string>
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@
|
|||
|
||||
<string name="home_lexicon">Lexicon</string>
|
||||
<string name="home_quest_log">Quest logs</string>
|
||||
<string name="home_location">Location</string>
|
||||
|
||||
<string name="lexicon_search">Search</string>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue