override theme option

This commit is contained in:
qimiko committed 2024-01-08 01:23:03 -07:00
1 parent d30d4cb008
commit 3843d2c94e
5 files changed
+248 -14

No files matched your search

@@ -15,6 +15,7 @@ import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -32,6 +33,8 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.geode.launcher.api.ReleaseViewModel import com.geode.launcher.api.ReleaseViewModel
import com.geode.launcher.ui.theme.GeodeLauncherTheme import com.geode.launcher.ui.theme.GeodeLauncherTheme
import com.geode.launcher.ui.theme.LocalTheme
import com.geode.launcher.ui.theme.Theme
import com.geode.launcher.ui.theme.Typography import com.geode.launcher.ui.theme.Typography
import com.geode.launcher.utils.LaunchUtils import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils import com.geode.launcher.utils.PreferenceUtils
@@ -48,7 +51,11 @@ class MainActivity : ComponentActivity() {
val geodeInstalled = LaunchUtils.isGeodeInstalled(this) val geodeInstalled = LaunchUtils.isGeodeInstalled(this)
setContent { setContent {
GeodeLauncherTheme { val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
val theme = Theme.fromInt(themeOption)
CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme) {
Surface( Surface(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background color = MaterialTheme.colorScheme.background
@@ -57,6 +64,7 @@ class MainActivity : ComponentActivity() {
} }
} }
} }
}
if (gdInstalled && geodeInstalled) { if (gdInstalled && geodeInstalled) {
intent.getBooleanExtra("restarted", false).let { intent.getBooleanExtra("restarted", false).let {
if (it) { if (it) {
@@ -362,7 +370,7 @@ fun MainScreenLightPreview() {
@Preview(showSystemUi = true) @Preview(showSystemUi = true)
@Composable @Composable
fun MainScreenDarkPreview() { fun MainScreenDarkPreview() {
GeodeLauncherTheme(darkTheme = true) { GeodeLauncherTheme(theme = Theme.DARK) {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
MainScreen() MainScreen()
} }
@@ -1,5 +1,6 @@
package com.geode.launcher package com.geode.launcher
import android.app.UiModeManager
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
@@ -10,37 +11,43 @@ import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedDispatcher import androidx.activity.OnBackPressedDispatcher
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.documentfile.provider.DocumentFile import androidx.compose.ui.window.Dialog
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.geode.launcher.api.ReleaseViewModel import com.geode.launcher.api.ReleaseViewModel
import com.geode.launcher.ui.theme.GeodeLauncherTheme import com.geode.launcher.ui.theme.GeodeLauncherTheme
import com.geode.launcher.ui.theme.LocalTheme
import com.geode.launcher.ui.theme.Theme
import com.geode.launcher.ui.theme.Typography import com.geode.launcher.ui.theme.Typography
import com.geode.launcher.utils.LaunchUtils import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils import com.geode.launcher.utils.PreferenceUtils
import java.io.File
import java.net.ConnectException import java.net.ConnectException
import java.net.UnknownHostException import java.net.UnknownHostException
@@ -48,7 +55,11 @@ class SettingsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
GeodeLauncherTheme { val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
val theme = Theme.fromInt(themeOption)
CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme) {
Surface( Surface(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background color = MaterialTheme.colorScheme.background
@@ -61,6 +72,7 @@ class SettingsActivity : ComponentActivity() {
} }
} }
} }
}
/* /*
fun onExportSaveData(context: Context) { fun onExportSaveData(context: Context) {
@@ -156,6 +168,36 @@ fun UpdateIndicator(
} }
} }
@Composable
fun themeToKey(theme: Int): String {
return when (theme) {
1 -> stringResource(R.string.preference_theme_light)
2 -> stringResource(R.string.preference_theme_dark)
else -> stringResource(R.string.preference_theme_default)
}
}
fun updateTheme(context: Context, theme: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
uiModeManager.setApplicationNightMode(
when (theme) {
1 -> UiModeManager.MODE_NIGHT_NO
2 -> UiModeManager.MODE_NIGHT_YES
else -> UiModeManager.MODE_NIGHT_CUSTOM
}
)
} else {
AppCompatDelegate.setDefaultNightMode(
when (theme) {
1 -> AppCompatDelegate.MODE_NIGHT_NO
2 -> AppCompatDelegate.MODE_NIGHT_YES
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
)
}
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SettingsScreen( fun SettingsScreen(
@@ -179,11 +221,14 @@ fun SettingsScreen(
} }
} }
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
Scaffold( Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
snackbarHost = { snackbarHost = {
SnackbarHost(hostState = snackbarHostState) SnackbarHost(hostState = snackbarHostState)
}, },
topBar = { topBar = {
// todo: the TopAppBar makes theme transitions look bad. why is that
TopAppBar( TopAppBar(
navigationIcon = { navigationIcon = {
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) { IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
@@ -200,6 +245,7 @@ fun SettingsScreen(
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
}, },
scrollBehavior = scrollBehavior,
) )
}, },
content = { innerPadding -> content = { innerPadding ->
@@ -211,6 +257,14 @@ fun SettingsScreen(
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
OptionsGroup(context.getString(R.string.preference_category_testing)) { OptionsGroup(context.getString(R.string.preference_category_testing)) {
SettingsSelectCard(
title = stringResource(R.string.preference_theme_name),
dialogTitle = stringResource(R.string.preference_theme_select),
maxVal = 2,
preferenceKey = PreferenceUtils.Key.THEME,
toLabel = { themeToKey(it) },
extraSelectBehavior = { updateTheme(context, it) }
)
SettingsCard( SettingsCard(
title = context.getString(R.string.preference_load_testing_name), title = context.getString(R.string.preference_load_testing_name),
preferenceKey = PreferenceUtils.Key.LOAD_TESTING, preferenceKey = PreferenceUtils.Key.LOAD_TESTING,
@@ -302,6 +356,127 @@ fun OptionsGroup(title: String, content: @Composable () -> Unit) {
} }
} }
@Composable
fun SettingsSelectCard(
title: String,
dialogTitle: String,
maxVal: Int,
preferenceKey: PreferenceUtils.Key,
toLabel: @Composable (Int) -> String,
extraSelectBehavior: ((Int) -> Unit)? = null
) {
val preferenceValue by PreferenceUtils.useIntPreference(preferenceKey)
var showDialog by remember { mutableStateOf(false) }
OptionsCard(
title = { OptionsTitle(title = title) },
modifier = Modifier
.clickable(
onClick = {
showDialog = true
},
role = Role.Button
)
) {
Text(toLabel(preferenceValue))
}
if (showDialog) {
val context = LocalContext.current
SelectDialog(
title = dialogTitle,
onDismissRequest = {
showDialog = false
},
onSelect = { selected ->
showDialog = false
PreferenceUtils.get(context)
.setInt(preferenceKey, selected)
extraSelectBehavior?.invoke(selected)
},
initialValue = preferenceValue,
toLabel = toLabel,
optionsCount = maxVal
)
}
}
@Composable
fun SelectDialog(
title: String,
onDismissRequest: () -> Unit,
onSelect: (Int) -> Unit,
initialValue: Int,
toLabel: @Composable (Int) -> String,
optionsCount: Int,
) {
var selectedValue by remember { mutableIntStateOf(initialValue) }
Dialog(onDismissRequest = onDismissRequest) {
Card(
shape = RoundedCornerShape(16.dp)
) {
// styling a dialog is actually a little hard if you're doing what i'm doing
// maybe there's a better way to make these padding values...
Column {
Text(
title,
style = Typography.titleLarge,
modifier = Modifier.padding(
start = 28.dp,
top = 24.dp,
bottom = 12.dp
)
)
// do not give the row or column padding!! it messes up the selection effect
(0..optionsCount).forEach { id ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable(
onClick = { selectedValue = id },
role = Role.RadioButton
)
.padding(horizontal = 12.dp)
) {
RadioButton(
selected = selectedValue == id,
onClick = { selectedValue = id }
)
Text(
toLabel(id),
style = Typography.bodyMedium
)
}
}
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier
.fillMaxWidth()
.padding(
bottom = 16.dp,
end = 16.dp,
top = 4.dp
)
) {
TextButton(onClick = onDismissRequest) {
Text(stringResource(R.string.message_box_cancel))
}
TextButton(onClick = { onSelect(selectedValue) }) {
Text(stringResource(R.string.message_box_accept))
}
}
}
}
}
}
@Composable @Composable
fun OptionsButton(title: String, description: String? = null, onClick: () -> Unit) { fun OptionsButton(title: String, description: String? = null, onClick: () -> Unit) {
OptionsCard( OptionsCard(
@@ -10,10 +10,13 @@ import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme( private val DarkColorScheme = darkColorScheme(
primary = Purple80, primary = Purple80,
@@ -37,9 +40,24 @@ private val LightColorScheme = lightColorScheme(
*/ */
) )
enum class Theme {
LIGHT, DARK;
companion object {
@Composable
fun fromInt(value: Int) = when (value) {
1 -> LIGHT
2 -> DARK
else -> if (isSystemInDarkTheme()) DARK else LIGHT
}
}
}
val LocalTheme = compositionLocalOf { Theme.LIGHT }
@Composable @Composable
fun GeodeLauncherTheme( fun GeodeLauncherTheme(
darkTheme: Boolean = isSystemInDarkTheme(), theme: Theme = Theme.fromInt(0),
// Dynamic color is available on Android 12+ // Dynamic color is available on Android 12+
dynamicColor: Boolean = true, dynamicColor: Boolean = true,
content: @Composable () -> Unit content: @Composable () -> Unit
@@ -47,22 +65,28 @@ fun GeodeLauncherTheme(
val colorScheme = when { val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) when (theme) {
Theme.DARK -> dynamicDarkColorScheme(context)
else -> dynamicLightColorScheme(context)
} }
darkTheme -> DarkColorScheme }
theme == Theme.DARK -> DarkColorScheme
else -> LightColorScheme else -> LightColorScheme
} }
val view = LocalView.current val view = LocalView.current
if (!view.isInEditMode) { if (!view.isInEditMode) {
SideEffect { SideEffect {
with ((view.context as Activity).window) { with ((view.context as Activity).window) {
statusBarColor = colorScheme.background.toArgb() WindowCompat.setDecorFitsSystemWindows(this, false)
navigationBarColor = colorScheme.background.toArgb()
statusBarColor = Color.Transparent.toArgb()
navigationBarColor = Color.Transparent.toArgb()
} }
with (ViewCompat.getWindowInsetsController(view)) { with (ViewCompat.getWindowInsetsController(view)) {
this?.isAppearanceLightStatusBars = !darkTheme this?.isAppearanceLightStatusBars = theme == Theme.LIGHT
this?.isAppearanceLightNavigationBars = !darkTheme this?.isAppearanceLightNavigationBars = theme == Theme.LIGHT
} }
} }
} }
@@ -2,7 +2,6 @@ package com.geode.launcher.utils
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
@@ -36,6 +35,13 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
} }
} }
@Composable
fun useIntPreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current): MutableState<Int> {
return usePreference(preferenceKey, lifecycleOwner) { p, k ->
p.getInt(k)
}
}
@Composable @Composable
private fun <T> usePreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, preferenceGet: (PreferenceUtils, Key) -> T): MutableState<T> { private fun <T> usePreference(preferenceKey: Key, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, preferenceGet: (PreferenceUtils, Key) -> T): MutableState<T> {
val context = LocalContext.current val context = LocalContext.current
@@ -88,7 +94,8 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
UPDATE_AUTOMATICALLY, UPDATE_AUTOMATICALLY,
RELEASE_CHANNEL, RELEASE_CHANNEL,
CURRENT_VERSION_TAG, CURRENT_VERSION_TAG,
CURRENT_VERSION_TIMESTAMP CURRENT_VERSION_TIMESTAMP,
THEME
} }
private fun defaultValueForBooleanKey(key: Key): Boolean { private fun defaultValueForBooleanKey(key: Key): Boolean {
@@ -106,6 +113,7 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
Key.RELEASE_CHANNEL -> "PreferenceReleaseChannel" Key.RELEASE_CHANNEL -> "PreferenceReleaseChannel"
Key.CURRENT_VERSION_TAG -> "PreferenceCurrentVersionName" Key.CURRENT_VERSION_TAG -> "PreferenceCurrentVersionName"
Key.CURRENT_VERSION_TIMESTAMP -> "PreferenceCurrentVersionDescriptor" Key.CURRENT_VERSION_TIMESTAMP -> "PreferenceCurrentVersionDescriptor"
Key.THEME -> "PreferenceTheme"
} }
} }
@@ -150,4 +158,16 @@ class PreferenceUtils(private val sharedPreferences: SharedPreferences) {
putLong(keyName, value) putLong(keyName, value)
} }
} }
fun getInt(key: Key): Int {
val keyName = keyToName(key)
return sharedPreferences.getInt(keyName, 0)
}
fun setInt(key: Key, value: Int) {
val keyName = keyToName(key)
sharedPreferences.edit {
putInt(keyName, value)
}
}
} }
+7
View File
@@ -30,6 +30,7 @@
<string name="release_fetch_button_cancel">Cancel</string> <string name="release_fetch_button_cancel">Cancel</string>
<!-- GeometryDashActivity strings --> <!-- GeometryDashActivity strings -->
<string name="message_box_cancel">Cancel</string>
<string name="message_box_accept">OK</string> <string name="message_box_accept">OK</string>
<!-- SettingsActivity strings --> <!-- SettingsActivity strings -->
@@ -46,6 +47,12 @@
<string name="preferences_copy_external_button">Copy external files directory</string> <string name="preferences_copy_external_button">Copy external files directory</string>
<string name="preferences_open_file_manager">Open file manager</string> <string name="preferences_open_file_manager">Open file manager</string>
<string name="preference_theme_name">Theme</string>
<string name="preference_theme_select">Select theme</string>
<string name="preference_theme_default">System default</string>
<string name="preference_theme_light">Light</string>
<string name="preference_theme_dark">Dark</string>
<string name="preference_category_updater">Updates</string> <string name="preference_category_updater">Updates</string>
<string name="preference_update_automatically_name">Enable automatic updates</string> <string name="preference_update_automatically_name">Enable automatic updates</string>
<string name="preference_release_channel_name">Use nightly release channel</string> <string name="preference_release_channel_name">Use nightly release channel</string>