Add a settings screen.

This commit is contained in:
Andres Gomez, Thomas (ITDV RL) 2024-06-10 09:23:48 +02:00
parent 90bf11909f
commit a6124cbe79
28 changed files with 668 additions and 63 deletions

View file

@ -161,6 +161,9 @@ dependencies {
implementation("androidx.room:room-ktx:2.6.1")
ksp("androidx.room:room-compiler:2.6.1")
// DataStore
implementation("androidx.datastore:datastore-preferences:1.1.1")
// Image
implementation("io.coil-kt:coil-compose:2.6.0")
}

View file

@ -6,7 +6,6 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.navigationBarsPadding
@ -57,6 +56,7 @@ val LocalRollOverlay = compositionLocalOf<BlurredRollOverlayHostState> {
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val themeViewModel: ThemeViewModel by viewModels()
private val launcherViewModel: LauncherViewModel by viewModels()
private val rollViewModel: RollOverlayViewModel by viewModels()
@ -72,16 +72,18 @@ class MainActivity : ComponentActivity() {
}
setContent {
LexiconTheme {
val snack = remember { SnackbarHostState() }
val overlay = rememberBlurredRollOverlayHostState(
viewModel = rollViewModel,
)
val snack = remember { SnackbarHostState() }
val overlay = rememberBlurredRollOverlayHostState(
viewModel = rollViewModel,
)
CompositionLocalProvider(
LocalActivity provides this,
LocalSnack provides snack,
LocalRollOverlay provides overlay,
CompositionLocalProvider(
LocalActivity provides this,
LocalSnack provides snack,
LocalRollOverlay provides overlay,
) {
LexiconTheme(
isDarkTheme = themeViewModel.useDarkTheme.value
) {
Scaffold(
contentWindowInsets = NO_WINDOW_INSETS,
@ -106,7 +108,7 @@ class MainActivity : ComponentActivity() {
}
},
snackbarHost = {
val isDarkTheme = isSystemInDarkTheme()
val isDarkTheme = themeViewModel.useDarkTheme.value
val elevation = remember {
derivedStateOf { if (isDarkTheme) 2.dp else 0.dp }
}
@ -145,15 +147,17 @@ class MainActivity : ComponentActivity() {
}
}
)
BackHandler(enabled = overlay.isOverlayVisible) {
overlay.hideOverlay()
}
}
HandleFetchError(
snack = snack,
errors = launcherViewModel.error,
)
BackHandler(
enabled = overlay.isOverlayVisible,
onBack = { overlay.hideOverlay() },
)
HandleFetchError(
snack = snack,
errors = launcherViewModel.error,
)
}
}
}
}

View file

@ -0,0 +1,52 @@
package com.pixelized.rplexicon
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalView
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import com.pixelized.rplexicon.data.repository.preferences.PreferencesRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class ThemeViewModel @Inject constructor(
private val preferencesRepository: PreferencesRepository,
) : ViewModel() {
val useDarkTheme: State<Boolean>
@Composable
@Stable
get() {
val systemDarkTheme = isSystemInDarkTheme()
val useDarkTheme = preferencesRepository.useDarkThemeFlow.collectAsState(
initial = preferencesRepository.useDarkThemeFlow.value ?: systemDarkTheme
)
return remember {
derivedStateOf {
useDarkTheme.value ?: systemDarkTheme
}
}
}
suspend fun updateDarkThemeUsage(useDarkTheme: Boolean) {
preferencesRepository.updateUseDarkTheme(useDarkTheme = useDarkTheme)
}
}
@Composable
@Stable
fun isInDarkTheme(): Boolean {
val view = LocalView.current
return if (view.isInEditMode) {
isSystemInDarkTheme()
} else {
val themeViewModel: ThemeViewModel = hiltViewModel()
themeViewModel.useDarkTheme.value
}
}

View file

@ -7,4 +7,5 @@ data class LexiconConfig(
val featureQuests: Boolean,
val featureSummary: Boolean,
val featureSearch: Boolean,
val featureOther: Boolean,
)

View file

@ -24,6 +24,7 @@ class RemoteConfigRepository @Inject constructor() {
featureQuests = DEFAULT[FEATURE_QUESTS] as Boolean,
featureSummary = DEFAULT[FEATURE_SUMMARY] as Boolean,
featureSearch = DEFAULT[FEATURE_SEARCH] as Boolean,
featureOther = DEFAULT[FEATURE_OTHER] as Boolean,
)
)
val config: StateFlow<LexiconConfig> get() = _config
@ -54,6 +55,7 @@ class RemoteConfigRepository @Inject constructor() {
featureQuests = firebase.getBoolean(FEATURE_QUESTS),
featureSummary = firebase.getBoolean(FEATURE_SUMMARY),
featureSearch = firebase.getBoolean(FEATURE_SEARCH),
featureOther = firebase.getBoolean(FEATURE_OTHER),
)
_config.value = config
}
@ -70,6 +72,7 @@ class RemoteConfigRepository @Inject constructor() {
private const val FEATURE_QUESTS = "feature_quests"
private const val FEATURE_SUMMARY = "feature_summary"
private const val FEATURE_SEARCH = "feature_search"
private const val FEATURE_OTHER = "feature_other"
private val DEFAULT: HashMap<String, Any?> = hashMapOf(
FEATURE_ADVENTURE to false,
@ -78,6 +81,7 @@ class RemoteConfigRepository @Inject constructor() {
FEATURE_QUESTS to false,
FEATURE_SUMMARY to false,
FEATURE_SEARCH to false,
FEATURE_OTHER to false,
)
}
}

View file

@ -0,0 +1,47 @@
package com.pixelized.rplexicon.data.repository.preferences
import android.content.Context
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class PreferencesRepository @Inject constructor(
@ApplicationContext context: Context
) {
private val Context.dataStore by preferencesDataStore(name = "user_preferences")
private val dataStore = context.dataStore
val useDarkThemeFlow: StateFlow<Boolean?> = dataStore.data
.catch {
emit(emptyPreferences())
}.map { preferences ->
preferences[PreferencesKeys.USE_DARK_THEME]
}.stateIn(
scope = CoroutineScope(Dispatchers.Default + Job()),
started = SharingStarted.Eagerly,
initialValue = false,
)
suspend fun updateUseDarkTheme(useDarkTheme: Boolean) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.USE_DARK_THEME] = useDarkTheme
}
}
object PreferencesKeys {
val USE_DARK_THEME = booleanPreferencesKey("use_dark_theme")
}
}

View file

@ -4,6 +4,7 @@ import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row

View file

@ -74,7 +74,7 @@ fun BackgroundImage(
fun rememberSaturationFilter(
saturation: Float = 0f,
): ColorFilter {
return remember {
return remember(saturation) {
ColorFilter.colorMatrix(
ColorMatrix().also { it.setToSaturation(saturation) }
)
@ -86,7 +86,7 @@ fun rememberBackgroundGradient(
vararg gradients: Float,
): Brush {
val colorScheme = MaterialTheme.colorScheme
return remember {
return remember(colorScheme) {
Brush.verticalGradient(
colors = gradients.map { colorScheme.surface.copy(alpha = it) }
)
@ -99,7 +99,7 @@ fun rememberBackgroundGradient(
to: Float = 1.0f,
): Brush {
val colorScheme = MaterialTheme.colorScheme
return remember {
return remember(colorScheme) {
Brush.verticalGradient(
colors = listOf(
colorScheme.surface.copy(alpha = from),

View file

@ -1,6 +1,7 @@
package com.pixelized.rplexicon.ui.composable
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.runtime.Composable
@ -50,4 +51,25 @@ fun rememberAnimatedShadow(
targetValue = shadowTarget.value,
label = "animated shadow",
)
}
@Composable
fun rememberAnimatedShadow(
scrollState: ScrollState,
rest: Dp = 0.dp,
target: Dp = 4.dp,
): State<Dp> {
val shadowTarget = remember(scrollState) {
derivedStateOf {
if (scrollState.value > 0) {
target
} else {
rest
}
}
}
return animateDpAsState(
targetValue = shadowTarget.value,
label = "animated shadow",
)
}

View file

@ -10,9 +10,9 @@ import androidx.navigation.NavOptionsBuilder
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import com.pixelized.rplexicon.ui.navigation.screens.AUTHENTICATION_ROUTE
import com.pixelized.rplexicon.ui.navigation.screens.composableAdventureDetail
import com.pixelized.rplexicon.ui.navigation.screens.composableAdventureBooks
import com.pixelized.rplexicon.ui.navigation.screens.composableAdventureChapters
import com.pixelized.rplexicon.ui.navigation.screens.composableAdventureDetail
import com.pixelized.rplexicon.ui.navigation.screens.composableAuthentication
import com.pixelized.rplexicon.ui.navigation.screens.composableCharacterSheet
import com.pixelized.rplexicon.ui.navigation.screens.composableLanding
@ -23,6 +23,7 @@ import com.pixelized.rplexicon.ui.navigation.screens.composableLocations
import com.pixelized.rplexicon.ui.navigation.screens.composableQuestDetail
import com.pixelized.rplexicon.ui.navigation.screens.composableQuests
import com.pixelized.rplexicon.ui.navigation.screens.composableSearch
import com.pixelized.rplexicon.ui.navigation.screens.composableSettings
import com.pixelized.rplexicon.ui.navigation.screens.composableSpellDetail
import com.pixelized.rplexicon.ui.navigation.screens.composableSummary
import com.pixelized.rplexicon.ui.navigation.screens.navigateToLanding
@ -66,6 +67,7 @@ fun ScreenNavHost(
composableAdventureBooks()
composableAdventureChapters()
composableAdventureDetail()
composableSettings()
}
}
}

View file

@ -0,0 +1,28 @@
package com.pixelized.rplexicon.ui.navigation.screens
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.NavOptionsBuilder
import com.pixelized.rplexicon.ui.navigation.NavigationAnimation
import com.pixelized.rplexicon.ui.navigation.animatedComposable
import com.pixelized.rplexicon.ui.screens.settings.SettingsScreen
private val ROUTE = "settings"
val SETTINGS_ROUTE = ROUTE
fun NavGraphBuilder.composableSettings() {
animatedComposable(
route = SETTINGS_ROUTE,
animation = NavigationAnimation.Push,
) {
SettingsScreen()
}
}
fun NavHostController.navigateToSettings(
option: NavOptionsBuilder.() -> Unit = {},
) {
val route = ROUTE
navigate(route = route, builder = option)
}

View file

@ -32,6 +32,7 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -52,7 +53,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.credentials.exceptions.GetCredentialCancellationException
import androidx.credentials.exceptions.GetCredentialProviderConfigurationException
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.LocalActivity
import com.pixelized.rplexicon.LocalSnack
@ -61,6 +61,7 @@ import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.ui.theme.colors.GoogleColorPalette
import com.pixelized.rplexicon.utilitary.sensor.Gyroscope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlin.math.E
import kotlin.math.max
import kotlin.math.min
@ -90,6 +91,7 @@ fun AuthenticationScreen(
val context = LocalContext.current
val snack = LocalSnack.current
val activity = LocalActivity.current
val scope = rememberCoroutineScope()
Surface {
PartyBackground()
@ -101,7 +103,9 @@ fun AuthenticationScreen(
.padding(all = 16.dp),
version = versionVM.version,
onGoogleSignIn = {
authenticationVM.signIn(activity = activity)
scope.launch {
authenticationVM.signIn(activity = activity)
}
},
)
@ -344,7 +348,7 @@ private fun animatedWeight(
@Composable
private fun rememberBackgroundGradient(): Brush {
val colorScheme = MaterialTheme.colorScheme
return remember {
return remember(colorScheme) {
Brush.verticalGradient(
colors = listOf(
colorScheme.surface.copy(alpha = 0.25f),

View file

@ -36,8 +36,8 @@ class AuthenticationViewModel @Inject constructor(
AuthenticationStateUio.Initial
)
fun signIn(activity: Activity) {
viewModelScope.launch(Dispatchers.IO) {
suspend fun signIn(activity: Activity) {
withContext(Dispatchers.IO) {
try {
// create the credential manager
val credentialManager = CredentialManager.create(
@ -101,4 +101,14 @@ class AuthenticationViewModel @Inject constructor(
}
}
}
suspend fun signOut() {
withContext(Dispatchers.IO) {
Firebase.auth.signOut()
withContext(Dispatchers.Main) {
authenticationState.value = AuthenticationStateUio.Initial
}
}
}
}

View file

@ -58,7 +58,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.LocalRollOverlay
import com.pixelized.rplexicon.NO_WINDOW_INSETS
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.composable.Handle
import com.pixelized.rplexicon.ui.composable.KeepOnScreen
import com.pixelized.rplexicon.ui.composable.Loader
import com.pixelized.rplexicon.ui.composable.edit.HandleHitPointEditDialog

View file

@ -58,6 +58,7 @@ data class LandingItemUio(
QUEST,
MAP,
ADVENTURE,
OTHERS,
}
}

View file

@ -59,6 +59,7 @@ import com.pixelized.rplexicon.ui.navigation.screens.navigateToLexicon
import com.pixelized.rplexicon.ui.navigation.screens.navigateToLocation
import com.pixelized.rplexicon.ui.navigation.screens.navigateToQuestList
import com.pixelized.rplexicon.ui.navigation.screens.navigateToSearch
import com.pixelized.rplexicon.ui.navigation.screens.navigateToSettings
import com.pixelized.rplexicon.ui.navigation.screens.navigateToSummary
import com.pixelized.rplexicon.ui.screens.authentication.VersionViewModel
import com.pixelized.rplexicon.ui.screens.landing.LandingItemUio.Feature.ADVENTURE
@ -95,6 +96,7 @@ fun LandingScreen(
characters = viewModel.characterSheets,
tools = viewModel.toolFeatures,
encyclopedia = viewModel.lexiconFeatures,
others = viewModel.otherFeatures,
version = versionVM.version,
onFeature = {
when (it.feature) {
@ -130,6 +132,10 @@ fun LandingScreen(
ADVENTURE -> {
screen.navigateToAdventures()
}
LandingItemUio.Feature.OTHERS -> {
screen.navigateToSettings()
}
}
}
)
@ -150,6 +156,7 @@ private fun LandingContent(
characters: State<List<List<LandingItemUio>>>,
tools: State<List<List<LandingItemUio>>>,
encyclopedia: State<List<List<LandingItemUio>>>,
others: State<List<List<LandingItemUio>>>,
version: VersionViewModel.Version,
onFeature: (LandingItemUio) -> Unit,
) {
@ -334,6 +341,44 @@ private fun LandingContent(
}
}
if (others.value.isNotEmpty()) {
Text(
modifier = Modifier.padding(top = sectionPadding),
style = MaterialTheme.typography.labelSmall,
fontStyle = FontStyle.Italic,
fontWeight = FontWeight.Light,
text = stringResource(id = R.string.landing__caterogy__other),
)
}
others.value.forEach { group ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
) {
group.forEach { item ->
LandingItem(
modifier = Modifier
.weight(1f)
.aspectRatio(ratio = 1f),
imagePadding = PaddingValues(
top = 8.dp,
start = 16.dp,
end = 16.dp,
bottom = 24.dp,
),
item = item,
backgroundFilter = null,
backgroundGradientFrom = 0.0f,
backgroundGradientTo = 0.5f,
onClick = { onFeature(item) },
)
}
repeat(3 - group.size) {
Spacer(modifier = Modifier.weight(1f))
}
}
}
Text(
modifier = Modifier
.align(alignment = Alignment.End)
@ -383,8 +428,8 @@ private fun Modifier.magic(): Modifier = composed {
}
@Composable
@Preview(uiMode = UI_MODE_NIGHT_NO, heightDp = 1332)
@Preview(uiMode = UI_MODE_NIGHT_YES, heightDp = 1332)
@Preview(uiMode = UI_MODE_NIGHT_NO, heightDp = 1452)
@Preview(uiMode = UI_MODE_NIGHT_YES, heightDp = 1452)
private fun LandingPreview() {
LexiconTheme {
Surface {
@ -484,6 +529,20 @@ private fun LandingPreview() {
),
)
},
others = remember {
mutableStateOf(
listOf(
listOf(
LandingItemUio(
feature = LandingItemUio.Feature.OTHERS,
title = "Settings",
subTitle = null,
icon = R.drawable.icbg_item_foundry_misc_gear_a,
),
)
)
)
},
version = VersionViewModel.Version(R.string.app_name, "0.0.0", "0", true),
onFeature = { },
)

View file

@ -137,4 +137,30 @@ class LandingViewModel @Inject constructor(
}
}.collectAsState(initial = emptyList())
}
val otherFeatures: State<List<List<LandingItemUio>>>
@Composable
get() {
val context = LocalContext.current
return remember {
configRepository.config
.map { config ->
listOfNotNull(
when (config.featureOther) {
true -> LandingItemUio(
feature = LandingItemUio.Feature.OTHERS,
title = context.getString(R.string.settings__title),
subTitle = null,
icon = R.drawable.icbg_item_foundry_misc_gear_a,
)
else -> null
}
)
}
.map { items ->
items.groupBy { items.indexOf(it) / 3 }.values.toList()
}
}.collectAsState(initial = emptyList())
}
}

View file

@ -15,6 +15,7 @@ import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -84,6 +85,7 @@ import com.pixelized.rplexicon.ui.screens.rolls.composable.RollDiceUio
import com.pixelized.rplexicon.ui.screens.rolls.composable.ThrowsCard
import com.pixelized.rplexicon.ui.screens.rolls.composable.ThrowsCardUio
import com.pixelized.rplexicon.ui.screens.rolls.preview.rememberRollAlterations
import com.pixelized.rplexicon.isInDarkTheme
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import kotlinx.coroutines.launch
@ -98,6 +100,7 @@ fun RollOverlay(
RollOverlayContent(
modifier = Modifier.fillMaxSize(),
isDarkTheme = isInDarkTheme(),
drawer = drawer,
dice = viewModel.dice,
card = viewModel.card,
@ -145,6 +148,7 @@ fun RollOverlay(
@Composable
private fun RollOverlayContent(
modifier: Modifier = Modifier,
isDarkTheme: Boolean,
drawer: DrawerState,
dice: State<RollDiceUio?>,
card: State<ThrowsCardUio?>,
@ -321,6 +325,7 @@ private fun RollOverlayContent(
modifier = Modifier
.padding(bottom = if (enableDrawer.value) 32.dp else 0.dp)
.padding(all = 16.dp),
isDarkTheme = isDarkTheme,
throws = it,
showDetail = showDetail,
onClick = onCard,
@ -358,6 +363,7 @@ private fun RollOverlayPreview(
Surface {
RollOverlayContent(
modifier = Modifier.fillMaxSize(),
isDarkTheme = isSystemInDarkTheme(),
drawer = rememberDrawerState(initialValue = preview.drawer),
dice = preview.dice,
card = preview.card,

View file

@ -11,7 +11,6 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -52,6 +51,7 @@ import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.agsl.dancingColor
import com.pixelized.rplexicon.ui.agsl.rememberTimeState
import com.pixelized.rplexicon.ui.screens.rolls.composable.ThrowsCardUio.Throw.Type
import com.pixelized.rplexicon.isInDarkTheme
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.annotatedSpan
import com.pixelized.rplexicon.utilitary.extentions.lexicon
@ -98,12 +98,12 @@ data class ThrowsCardUio(
@Composable
fun ThrowsCard(
modifier: Modifier = Modifier,
isDarkTheme: Boolean = isInDarkTheme(),
throws: ThrowsCardUio,
showDetail: State<Boolean>,
onClick: () -> Unit,
) {
val density = LocalDensity.current
val isDarkMode = isSystemInDarkTheme()
val colorScheme = MaterialTheme.colorScheme
val typography = MaterialTheme.typography
val highlight = remember { SpanStyle(color = colorScheme.primary) }
@ -118,10 +118,13 @@ fun ThrowsCard(
Surface(
modifier = modifier
.fillMaxWidth()
.ddBorder(inner = inner, outline = remember { CutCornerShape(size = 16.dp) })
.ddBorder(
inner = inner,
outline = remember { CutCornerShape(size = 16.dp) },
)
.clip(shape = inner)
.clickable(onClick = onClick),
tonalElevation = if (isDarkMode) 4.dp else 0.dp,
tonalElevation = if (isDarkTheme) 4.dp else 0.dp,
) {
Column(
modifier = Modifier

View file

@ -0,0 +1,150 @@
package com.pixelized.rplexicon.ui.screens.settings
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
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.R
import com.pixelized.rplexicon.ThemeViewModel
import com.pixelized.rplexicon.ui.composable.rememberAnimatedShadow
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.navigation.rootOption
import com.pixelized.rplexicon.ui.navigation.screens.navigateToAuthentication
import com.pixelized.rplexicon.ui.screens.authentication.AuthenticationViewModel
import com.pixelized.rplexicon.ui.screens.settings.composable.ButtonPreference
import com.pixelized.rplexicon.ui.screens.settings.composable.SwitchPreference
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import kotlinx.coroutines.launch
@Composable
fun SettingsScreen(
themeViewModel: ThemeViewModel = hiltViewModel(),
authenticationViewModel: AuthenticationViewModel = hiltViewModel(),
) {
val screen = LocalScreenNavHost.current
val scope = rememberCoroutineScope()
Surface(
modifier = Modifier.fillMaxSize(),
) {
SettingsContent(
modifier = Modifier
.fillMaxSize()
.systemBarsPadding(),
isInDarkTheme = themeViewModel.useDarkTheme.value,
onBack = {
screen.popBackStack()
},
onThemeChange = {
scope.launch {
themeViewModel.updateDarkThemeUsage(useDarkTheme = it)
}
},
onLogout = {
scope.launch {
authenticationViewModel.signOut()
screen.navigateToAuthentication(option = rootOption())
}
},
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun SettingsContent(
modifier: Modifier = Modifier,
isInDarkTheme: Boolean,
scrollState: ScrollState = rememberScrollState(),
onBack: () -> Unit,
onThemeChange: (Boolean) -> Unit,
onLogout: () -> Unit,
) {
Scaffold(
modifier = modifier,
topBar = {
val shadow = rememberAnimatedShadow(scrollState = scrollState)
TopAppBar(
modifier = Modifier.shadow(elevation = shadow.value),
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
painter = painterResource(id = R.drawable.ic_arrow_back_ios_new_24),
contentDescription = null
)
}
},
title = {
Text(text = stringResource(id = R.string.settings__title))
},
)
},
content = { paddings ->
Surface(
modifier = Modifier.verticalScroll(state = scrollState)
) {
Column(
modifier = Modifier.padding(paddingValues = paddings),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
ButtonPreference(
title = stringResource(id = R.string.settings__logout__title),
description = stringResource(id = R.string.settings__logout__description),
action = stringResource(id = R.string.settings__logout__action),
onClick = onLogout,
)
HorizontalDivider()
SwitchPreference(
title = stringResource(id = R.string.settings__dark_theme__title),
description = stringResource(id = R.string.settings__dark_theme__description),
value = isInDarkTheme,
onCheckedChange = onThemeChange,
)
HorizontalDivider()
}
}
}
)
}
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
@Composable
private fun SettingsPreview() {
LexiconTheme {
SettingsContent(
modifier = Modifier.fillMaxSize(),
isInDarkTheme = false,
onBack = {},
onThemeChange = {},
onLogout = {},
)
}
}

View file

@ -0,0 +1,84 @@
package com.pixelized.rplexicon.ui.screens.settings.composable
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.theme.LexiconTheme
@Composable
fun ButtonPreference(
modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues(
start = 16.dp,
end = 8.dp,
top = 8.dp,
bottom = 8.dp,
),
title: String,
description: String,
action: String,
onClick: () -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(paddingValues = paddingValues)
.height(IntrinsicSize.Min)
.then(other = modifier),
horizontalArrangement = Arrangement.spacedBy(space = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(
modifier = Modifier.weight(weight = 1f),
) {
Text(
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
text = title
)
Text(
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Light,
text = description
)
}
TextButton(
onClick = onClick,
) {
Text(text = action)
}
}
}
@Composable
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun ButtonPreferencePreview() {
LexiconTheme {
ButtonPreference(
title = stringResource(id = R.string.settings__logout__title),
description = stringResource(id = R.string.settings__logout__description),
action = stringResource(id = R.string.settings__logout__action),
onClick = {},
)
}
}

View file

@ -0,0 +1,81 @@
package com.pixelized.rplexicon.ui.screens.settings.composable
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.ui.theme.LexiconTheme
@Composable
fun SwitchPreference(
modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
title: String,
description: String,
value: Boolean,
onCheckedChange: (Boolean) -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(paddingValues = paddingValues)
.height(IntrinsicSize.Min)
.then(other = modifier),
horizontalArrangement = Arrangement.spacedBy(space = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(
modifier = Modifier.weight(weight = 1f),
) {
Text(
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
text = title
)
Text(
style = MaterialTheme.typography.labelLarge,
fontWeight = FontWeight.Light,
text = description
)
}
Switch(
checked = value,
onCheckedChange = onCheckedChange
)
}
}
@Preview(uiMode = UI_MODE_NIGHT_NO)
@Preview(uiMode = UI_MODE_NIGHT_YES)
@Composable
private fun TogglePreferencePreview() {
LexiconTheme {
Surface {
SwitchPreference(
title = "Title",
description = "description",
value = true,
onCheckedChange = { },
)
}
}
}

View file

@ -8,6 +8,7 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalDensity
@ -40,12 +41,12 @@ data class LexiconTheme(
@Composable
fun LexiconTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
isDarkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val density = LocalDensity.current
val lexiconTheme = remember(density) {
val colorScheme = when (darkTheme) {
val lexiconTheme = remember(density, isDarkTheme) {
val colorScheme = when (isDarkTheme) {
true -> darkColorScheme()
else -> lightColorScheme()
}
@ -58,29 +59,29 @@ fun LexiconTheme(
)
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = lexiconTheme.colorScheme.status.toArgb()
window.navigationBarColor = lexiconTheme.colorScheme.navigation.toArgb()
WindowCompat.getInsetsController(window, view).let {
it.isAppearanceLightStatusBars = !darkTheme
it.isAppearanceLightNavigationBars = !darkTheme
}
}
}
CompositionLocalProvider(
LocalLexiconTheme provides lexiconTheme,
) {
val theme = LocalLexiconTheme.current
val view = LocalView.current
MaterialTheme(
colorScheme = lexiconTheme.colorScheme.base,
shapes = lexiconTheme.shapes.base,
typography = lexiconTheme.typography.base,
content = {
CompositionLocalProvider(
LocalLexiconTheme provides lexiconTheme,
) {
content()
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = theme.colorScheme.status.toArgb()
window.navigationBarColor = theme.colorScheme.navigation.toArgb()
WindowCompat.getInsetsController(window, view).let {
it.isAppearanceLightStatusBars = !isDarkTheme
it.isAppearanceLightNavigationBars = !isDarkTheme
}
}
}
)
MaterialTheme(
colorScheme = theme.colorScheme.base,
typography = theme.typography.base,
shapes = theme.shapes.base,
content = content,
)
}
}

View file

@ -9,7 +9,6 @@ import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
@ -43,6 +42,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.google.accompanist.placeholder.PlaceholderHighlight
import com.google.accompanist.placeholder.placeholder
import com.pixelized.rplexicon.isInDarkTheme
import com.pixelized.rplexicon.utilitary.extentions.lexicon
fun Modifier.placeholder(
@ -114,7 +114,7 @@ fun Modifier.ddBorder(
inner: Shape,
innerWidth: Dp = 1.dp,
): Modifier = composed {
val isDarkTheme = isSystemInDarkTheme()
val isDarkTheme = isInDarkTheme()
val elevation = remember { derivedStateOf { if (isDarkTheme) 2.dp else 0.dp } }
val colorScheme = MaterialTheme.lexicon.colorScheme
this then Modifier
@ -147,7 +147,8 @@ fun Modifier.ddBorder(
fun Modifier.lexiconShadow(): Modifier {
return this then composed {
if (isSystemInDarkTheme()) {
val isDarkTheme = isInDarkTheme()
if (isDarkTheme) {
val color = DividerDefaults.color
drawWithContent {
drawContent()

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View file

@ -239,6 +239,14 @@
<string name="landing__caterogy__character">Feuilles de personnage</string>
<string name="landing__caterogy__encyclopedia">Encyclopédie</string>
<string name="landing__caterogy__tools">Outils</string>
<string name="landing__caterogy__other">Autre</string>
<string name="adventures_title">Histoires &amp; Péripéties</string>
<string name="settings__title">Paramêtres</string>
<string name="settings__dark_theme__title">Theme sombre</string>
<string name="settings__dark_theme__description">Utilise un fond noir pour économiser la batterie.</string>
<string name="settings__logout__title">Authentification</string>
<string name="settings__logout__description">Déconnectez vous de votre compte google.</string>
<string name="settings__logout__action">Se déconnectez</string>
</resources>

View file

@ -50,6 +50,7 @@
<string name="landing__caterogy__character">Character\'s sheets</string>
<string name="landing__caterogy__tools">Tools</string>
<string name="landing__caterogy__encyclopedia">Encyclopedia</string>
<string name="landing__caterogy__other">Other</string>
<string name="landing__character_brulkhai" translatable="false">Brulkhai</string>
<string name="landing__character_leandre" translatable="false">Léandre</string>
<string name="landing__character_nelia" translatable="false">Nelia</string>
@ -246,4 +247,11 @@
<string name="summary__title">Game Master</string>
<string name="adventures_title">Stories &amp; Adventures</string>
<string name="settings__title">Settings</string>
<string name="settings__dark_theme__title">Dark theme</string>
<string name="settings__dark_theme__description">Use black background to save battery.</string>
<string name="settings__logout__title">Authentication</string>
<string name="settings__logout__description">Logout from your google account.</string>
<string name="settings__logout__action">Logout</string>
</resources>