Add pull to refresh on the Lexion.
This commit is contained in:
parent
6167999001
commit
c5fb8bf99e
7 changed files with 153 additions and 63 deletions
|
|
@ -64,6 +64,7 @@ android {
|
|||
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
|
|
@ -85,6 +86,7 @@ dependencies {
|
|||
implementation("androidx.compose.ui:ui:1.4.3")
|
||||
implementation("androidx.compose.ui:ui-graphics:1.4.3")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview:1.4.3")
|
||||
implementation("androidx.compose.material:material:1.4.3")
|
||||
implementation("androidx.compose.material3:material3:1.1.1")
|
||||
debugImplementation("androidx.compose.ui:ui-tooling:1.4.3")
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ package com.pixelized.rplexicon.ui.screens.authentication
|
|||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
|
@ -18,7 +19,6 @@ import androidx.compose.runtime.State
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
|
|
@ -38,17 +38,21 @@ import kotlinx.coroutines.CoroutineScope
|
|||
|
||||
@Composable
|
||||
fun AuthenticationScreen(
|
||||
viewModel: AuthenticationViewModel = hiltViewModel()
|
||||
authenticationVM: AuthenticationViewModel = hiltViewModel(),
|
||||
versionVM: VersionViewModel = hiltViewModel(),
|
||||
) {
|
||||
val screen = LocalScreenNavHost.current
|
||||
val activity = LocalActivity.current
|
||||
val state = viewModel.rememberAuthenticationState()
|
||||
val state = authenticationVM.rememberAuthenticationState()
|
||||
|
||||
Surface {
|
||||
AuthenticationScreenContent(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(all = 16.dp),
|
||||
version = versionVM.version,
|
||||
onGoogleSignIn = {
|
||||
viewModel.signIn(activity = activity)
|
||||
authenticationVM.signIn(activity = activity)
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -74,34 +78,27 @@ fun HandleAuthenticationState(
|
|||
@Composable
|
||||
private fun AuthenticationScreenContent(
|
||||
modifier: Modifier = Modifier,
|
||||
version: VersionViewModel.Version,
|
||||
onGoogleSignIn: () -> Unit,
|
||||
) {
|
||||
Box(
|
||||
Column(
|
||||
modifier = modifier,
|
||||
contentAlignment = Alignment.BottomCenter,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.Bottom),
|
||||
horizontalAlignment = Alignment.End,
|
||||
) {
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.padding(all = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
colors = ButtonDefaults.outlinedButtonColors(),
|
||||
colors = ButtonDefaults.buttonColors(),
|
||||
onClick = onGoogleSignIn,
|
||||
) {
|
||||
Text(text = rememeberGoogleStringResource())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberBackgroundGradient(): Brush {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
return remember {
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
colorScheme.surface.copy(alpha = 0.2f),
|
||||
colorScheme.surface.copy(alpha = 0.5f),
|
||||
colorScheme.surface.copy(alpha = 1.0f),
|
||||
)
|
||||
Text(
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
text = version.toText(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -175,8 +172,14 @@ private fun rememeberGoogleStringResource(): AnnotatedString {
|
|||
@Preview(uiMode = UI_MODE_NIGHT_YES)
|
||||
private fun AuthenticationScreenContentPreview() {
|
||||
LexiconTheme {
|
||||
AuthenticationScreenContent(
|
||||
onGoogleSignIn = { },
|
||||
)
|
||||
Surface {
|
||||
AuthenticationScreenContent(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(all = 16.dp),
|
||||
version = VersionViewModel.Version(R.string.app_name, "0.0.0", "0"),
|
||||
onGoogleSignIn = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package com.pixelized.rplexicon.ui.screens.authentication
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.pixelized.rplexicon.BuildConfig
|
||||
import com.pixelized.rplexicon.R
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class VersionViewModel @Inject constructor() : ViewModel() {
|
||||
|
||||
val version = Version(
|
||||
appName = R.string.app_name,
|
||||
version = BuildConfig.VERSION_NAME,
|
||||
code = BuildConfig.VERSION_CODE.toString()
|
||||
)
|
||||
|
||||
@Stable
|
||||
data class Version(
|
||||
@StringRes val appName: Int,
|
||||
val version: String,
|
||||
val code: String,
|
||||
) {
|
||||
@Composable
|
||||
@Stable
|
||||
fun toText(): String {
|
||||
return "${stringResource(id = appName)} ${version}.${code}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ class CharacterDetailViewModel @Inject constructor(
|
|||
val source = repository.data.value[savedStateHandle.characterDetailArgument.id]
|
||||
return CharacterDetailUio(
|
||||
name = source.name,
|
||||
diminutive = source.diminutive?.let { "./ $it" },
|
||||
diminutive = source.diminutive?.takeIf { it.isNotBlank() }?.let { "./ $it" },
|
||||
gender = when (source.gender) {
|
||||
Lexicon.Gender.MALE -> "homme"
|
||||
Lexicon.Gender.FEMALE -> "femme"
|
||||
|
|
|
|||
|
|
@ -7,13 +7,23 @@ import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
|||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.PullRefreshState
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
|
|
@ -21,11 +31,14 @@ 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.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.navigation.LocalScreenNavHost
|
||||
import com.pixelized.rplexicon.ui.navigation.navigateToCharacterDetail
|
||||
import com.pixelized.rplexicon.ui.screens.lexicon.LexiconErrorUio.Default
|
||||
|
|
@ -43,15 +56,28 @@ sealed class LexiconErrorUio {
|
|||
object Default : LexiconErrorUio()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun LexiconScreen(
|
||||
viewModel: LexiconViewModel = hiltViewModel(),
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val screen = LocalScreenNavHost.current
|
||||
|
||||
val refresh = rememberPullRefreshState(
|
||||
refreshing = viewModel.isLoading.value,
|
||||
onRefresh = {
|
||||
scope.launch {
|
||||
viewModel.fetchLexicon()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Surface {
|
||||
LexiconScreenContent(
|
||||
items = viewModel.items,
|
||||
refresh = refresh,
|
||||
refreshing = viewModel.isLoading,
|
||||
onItem = {
|
||||
screen.navigateToCharacterDetail(id = it.id)
|
||||
},
|
||||
|
|
@ -66,23 +92,49 @@ fun LexiconScreen(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun LexiconScreenContent(
|
||||
modifier: Modifier = Modifier,
|
||||
refresh: PullRefreshState,
|
||||
refreshing: State<Boolean>,
|
||||
items: State<List<LexiconItemUio>>,
|
||||
onItem: (LexiconItemUio) -> Unit,
|
||||
) {
|
||||
LazyColumn(
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
contentPadding = PaddingValues(vertical = 8.dp),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(text = stringResource(id = R.string.app_name))
|
||||
},
|
||||
)
|
||||
}
|
||||
) {
|
||||
items(items = items.value) {
|
||||
LexiconItem(
|
||||
Box(
|
||||
modifier = Modifier.padding(paddingValues = it),
|
||||
contentAlignment = Alignment.TopCenter,
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.clickable { onItem(it) }
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||
item = it,
|
||||
.fillMaxSize()
|
||||
.pullRefresh(state = refresh),
|
||||
contentPadding = PaddingValues(vertical = 8.dp),
|
||||
) {
|
||||
items(items = items.value) { item ->
|
||||
LexiconItem(
|
||||
modifier = Modifier
|
||||
.clickable { onItem(item) }
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp, horizontal = 16.dp),
|
||||
item = item,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
PullRefreshIndicator(
|
||||
refreshing = refreshing.value,
|
||||
state = refresh,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -116,6 +168,7 @@ fun HandleError(
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
@Preview(uiMode = UI_MODE_NIGHT_NO)
|
||||
@Preview(uiMode = UI_MODE_NIGHT_YES)
|
||||
|
|
@ -124,6 +177,11 @@ private fun LexiconScreenContentPreview() {
|
|||
Surface {
|
||||
LexiconScreenContent(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
refresh = rememberPullRefreshState(
|
||||
refreshing = false,
|
||||
onRefresh = {},
|
||||
),
|
||||
refreshing = remember { mutableStateOf(false) },
|
||||
items = remember {
|
||||
mutableStateOf(
|
||||
listOf(
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecovera
|
|||
import com.pixelized.rplexicon.model.Lexicon
|
||||
import com.pixelized.rplexicon.repository.LexiconRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -20,6 +19,9 @@ class LexiconViewModel @Inject constructor(
|
|||
private val repository: LexiconRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _isLoading = mutableStateOf(false)
|
||||
val isLoading: State<Boolean> get() = _isLoading
|
||||
|
||||
private val _items = mutableStateOf<List<LexiconItemUio>>(emptyList())
|
||||
val items: State<List<LexiconItemUio>> get() = _items
|
||||
|
||||
|
|
@ -52,6 +54,7 @@ class LexiconViewModel @Inject constructor(
|
|||
|
||||
suspend fun fetchLexicon() {
|
||||
try {
|
||||
_isLoading.value = true
|
||||
repository.fetchLexicon()
|
||||
}
|
||||
// user need to accept OAuth2 permission.
|
||||
|
|
@ -64,6 +67,10 @@ class LexiconViewModel @Inject constructor(
|
|||
Log.e(TAG, exception.message, exception)
|
||||
_error.emit(LexiconErrorUio.Default)
|
||||
}
|
||||
// clean the laoding state
|
||||
finally {
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -1,17 +1,13 @@
|
|||
package com.pixelized.rplexicon.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.WindowCompat
|
||||
|
||||
|
|
@ -25,42 +21,32 @@ private val LightColorScheme = lightColorScheme(
|
|||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun LexiconTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
// dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
// val context = LocalContext.current
|
||||
// if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
// }
|
||||
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
// val view = LocalView.current
|
||||
// if (!view.isInEditMode) {
|
||||
// SideEffect {
|
||||
// val window = (view.context as Activity).window
|
||||
// window.statusBarColor = colorScheme.primary.toArgb()
|
||||
// WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
|
||||
// }
|
||||
// }
|
||||
|
||||
val view = LocalView.current
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val window = (view.context as Activity).window
|
||||
colorScheme.background.toArgb().let {
|
||||
window.statusBarColor = it
|
||||
window.navigationBarColor = it
|
||||
}
|
||||
WindowCompat.getInsetsController(window, view).let {
|
||||
it.isAppearanceLightStatusBars = !darkTheme
|
||||
it.isAppearanceLightNavigationBars = !darkTheme
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue