diff --git a/app/src/main/java/com/pixelized/rplexicon/facotry/LocationParser.kt b/app/src/main/java/com/pixelized/rplexicon/facotry/LocationParser.kt index d5b7a27..e92089d 100644 --- a/app/src/main/java/com/pixelized/rplexicon/facotry/LocationParser.kt +++ b/app/src/main/java/com/pixelized/rplexicon/facotry/LocationParser.kt @@ -32,6 +32,7 @@ class LocationParser @Inject constructor() { sheetIndex = index, name = name, uri = uri, + marquees = emptyList(), ) } else { null diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/composable/error/FetchErrorUio.kt b/app/src/main/java/com/pixelized/rplexicon/ui/composable/error/FetchErrorUio.kt new file mode 100644 index 0000000..5bf850b --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/composable/error/FetchErrorUio.kt @@ -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, + 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)) + } + } + } +} \ 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 index 8b820ec..6490c59 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/HomeNavHost.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/HomeNavHost.kt @@ -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 { @@ -44,9 +45,11 @@ val LocalPageNavHost = staticCompositionLocalOf { @OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class) @Composable fun HomeNavHost( - lexiconListState: LazyListState, navHostController: NavHostController = rememberAnimatedNavController(), - bottomBarItems: List = rememberBottomBarItems(navHostController = navHostController), + bottomBarItems: List = 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 { 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) } + ) ) } } \ 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 58878b1..7a1aba4 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 @@ -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() diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/navigation/pages/ComposableLocation.kt b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/pages/ComposableLocation.kt new file mode 100644 index 0000000..afb85de --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/navigation/pages/ComposableLocation.kt @@ -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) +} \ No newline at end of file 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 index a6d6919..dd12c75 100644 --- 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 @@ -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) } } 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 index c554a18..c42cf5f 100644 --- 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 @@ -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 + ) } } diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/list/LexiconScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/list/LexiconScreen.kt index fb6ff78..09545d7 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/list/LexiconScreen.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/list/LexiconScreen.kt @@ -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, - 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) diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/list/LexiconViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/list/LexiconViewModel.kt index cff0d49..5f046db 100644 --- a/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/list/LexiconViewModel.kt +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/lexicon/list/LexiconViewModel.kt @@ -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>(emptyList()) val items: State> get() = _items - private val _error = MutableSharedFlow() - val error: SharedFlow get() = _error + private val _error = MutableSharedFlow() + val error: SharedFlow 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 { diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/list/LocationItemUio.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/list/LocationItemUio.kt new file mode 100644 index 0000000..1381d07 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/list/LocationItemUio.kt @@ -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 { + override val values: Sequence = sequenceOf( + LocationItemUio.preview(placeHolder = false), + LocationItemUio.preview(placeHolder = true), + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/list/LocationScreen.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/list/LocationScreen.kt new file mode 100644 index 0000000..57446c7 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/list/LocationScreen.kt @@ -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, + items: State>, + 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 = { }, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/list/LocationViewModel.kt b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/list/LocationViewModel.kt new file mode 100644 index 0000000..8981362 --- /dev/null +++ b/app/src/main/java/com/pixelized/rplexicon/ui/screens/location/list/LocationViewModel.kt @@ -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 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 -> + 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" + } +} \ 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 index 90c6fd9..3457b8a 100644 --- 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 @@ -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 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 index 8b444a3..7a6ee3a 100644 --- 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 @@ -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>(emptyList()) val items: State> get() = _items -// private val _error = MutableSharedFlow() -// val error: SharedFlow get() = _error + private val _error = MutableSharedFlow() + val error: SharedFlow 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 { diff --git a/app/src/main/res/drawable/ic_outline_scroll_24.xml b/app/src/main/res/drawable/ic_outline_scroll_24.xml new file mode 100644 index 0000000..802d5de --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_scroll_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 7c41da6..3d1f632 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -30,6 +30,7 @@ Lexique Journal de quĂȘtes + Cartes Rechercher diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c5e663d..db1eb4c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -30,6 +30,7 @@ Lexicon Quest logs + Location Search