Files
android-launcher/app/src/main/java/com/geode/launcher/preferences/SettingsActivity.kt
T
Chloe 3194dff7ff Bundle Geode with Play Store build (#88)
* add gradle task to download geode binaries

* disable automatic update on play build

* get download url from release api

* fix incorrect priorities between external library and prebundled

* bundle geode resources
this also fixes the release api fetch as i never tested it until now, oops

* add message for loading from files dir

* raise minimum gd version with bundled geode

* clear old resources prior to copying
2026-09-07 17:19:12 -07:00

812 lines
30 KiB
Kotlin

package com.geode.launcher.preferences
import android.app.UiModeManager
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.provider.DocumentsContract
import android.text.format.DateUtils
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedDispatcher
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.lifecycle.viewmodel.compose.viewModel
import com.geode.launcher.BuildConfig
import com.geode.launcher.R
import com.geode.launcher.UserDirectoryProvider
import com.geode.launcher.preferences.components.BackupButton
import com.geode.launcher.preferences.components.OptionsButton
import com.geode.launcher.preferences.components.OptionsCard
import com.geode.launcher.preferences.components.OptionsGroup
import com.geode.launcher.preferences.components.OptionsLabel
import com.geode.launcher.preferences.components.OptionsTitle
import com.geode.launcher.preferences.components.SafeModeDialog
import com.geode.launcher.preferences.components.SettingsCard
import com.geode.launcher.preferences.components.SettingsFPSCard
import com.geode.launcher.preferences.components.SettingsRangeCard
import com.geode.launcher.preferences.components.SettingsSelectCard
import com.geode.launcher.preferences.components.SettingsStringSelectCard
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.robotoMonoFamily
import com.geode.launcher.updater.ReleaseViewModel
import com.geode.launcher.utils.ApplicationIcon
import com.geode.launcher.utils.Constants
import com.geode.launcher.utils.GamePackageUtils
import com.geode.launcher.utils.IconUtils
import com.geode.launcher.utils.LaunchUtils
import com.geode.launcher.utils.PreferenceUtils
import kotlinx.coroutines.launch
import java.net.ConnectException
import java.net.UnknownHostException
import kotlin.math.roundToInt
import kotlin.time.Clock
class SettingsActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
setContent {
val themeOption by PreferenceUtils.useIntPreference(PreferenceUtils.Key.THEME)
val theme = Theme.fromInt(themeOption)
val backgroundOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.BLACK_BACKGROUND)
val dynamicColorOption by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DISABLE_USER_THEME)
CompositionLocalProvider(LocalTheme provides theme) {
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption, dynamicColor = !dynamicColorOption) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
SettingsScreen(
onBackPressedDispatcher = onBackPressedDispatcher
)
}
}
}
}
}
}
fun onOpenFileManager(context: Context) {
// future thoughts: Intent.ACTION_VIEW may not work on some devices
// the "correct" solution is to go through a list of possible intents/applications
// don't feel like writing that behavior. this is good enough
val intent = Intent(Intent.ACTION_VIEW).apply {
data = DocumentsContract.buildRootUri(
"${context.packageName}.user",
UserDirectoryProvider.ROOT
)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
)
}
try {
context.startActivity(intent)
} catch (_: ActivityNotFoundException) {
Toast.makeText(
context,
context.getText(R.string.no_activity_found), Toast.LENGTH_SHORT
).show()
}
}
@Composable
fun UpdateErrorDialog(
error: Exception,
onDismiss: () -> Unit
) {
val clipboard = LocalClipboard.current
val coroutineScope = rememberCoroutineScope()
val clipboardLabel = stringResource(R.string.application_log_copy_label)
AlertDialog(
icon = {
Icon(
painterResource(R.drawable.icon_warning),
contentDescription = null,
)
},
title = { Text(stringResource(R.string.preference_check_for_updates_help_title)) },
text = {
SelectionContainer {
Text(
error.stackTraceToString(),
fontFamily = robotoMonoFamily,
style = Typography.bodyMedium,
modifier = Modifier.verticalScroll(rememberScrollState())
)
}
},
dismissButton = {
TextButton(onClick = {
coroutineScope.launch {
clipboard.setClipEntry(
ClipEntry(ClipData.newPlainText(clipboardLabel, error.stackTraceToString()))
)
}
}) {
Text(stringResource(R.string.preference_check_for_updates_help_copy))
}
},
confirmButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.message_box_accept))
}
},
onDismissRequest = onDismiss
)
}
@Composable
fun UpdateIndicator(
snackbarHostState: SnackbarHostState,
updateStatus: ReleaseViewModel.ReleaseUIState
) {
val context = LocalContext.current
val resources = LocalResources.current
var enablePopup by remember { mutableStateOf(false) }
var enableHelp by remember { mutableStateOf(false) }
var currentError by remember { mutableStateOf<Exception?>(null) }
val currentErrorS = currentError
if (currentErrorS != null && enableHelp) {
UpdateErrorDialog(currentErrorS) {
enableHelp = false
}
}
when (updateStatus) {
is ReleaseViewModel.ReleaseUIState.InUpdateCheck -> {
enablePopup = true
CircularProgressIndicator()
}
is ReleaseViewModel.ReleaseUIState.InDownload -> {
// is this the ideal design? idk
enablePopup = true
if (updateStatus.outOf != null) {
CircularProgressIndicator(
progress = {
val progress = updateStatus.downloaded / updateStatus.outOf.toDouble()
progress.toFloat()
},
)
} else {
CircularProgressIndicator()
}
}
else -> {}
}
LaunchedEffect(updateStatus) {
// only show popup if some progress was shown
if (!enablePopup) {
return@LaunchedEffect
}
when (updateStatus) {
is ReleaseViewModel.ReleaseUIState.Failure -> {
val message = when (updateStatus.exception) {
is UnknownHostException, is ConnectException ->
resources.getString(R.string.release_fetch_no_internet)
else -> resources.getString(R.string.preference_check_for_updates_failed)
}
currentError = updateStatus.exception
val snackbarResult = snackbarHostState.showSnackbar(message, resources.getString(R.string.preference_check_for_updates_failed_action))
if (snackbarResult == SnackbarResult.ActionPerformed) {
enableHelp = true
}
}
is ReleaseViewModel.ReleaseUIState.Finished -> {
if (updateStatus.hasUpdated) {
snackbarHostState.showSnackbar(
resources.getString(R.string.preference_check_for_updates_success)
)
} else {
val gameVersion = GamePackageUtils.getGameVersionCodeOrNull(context.packageManager)
if (gameVersion != null && gameVersion < Constants.SUPPORTED_VERSION_CODE) {
snackbarHostState.showSnackbar(
resources.getString(R.string.launcher_game_update_required_short)
)
} else {
snackbarHostState.showSnackbar(
resources.getString(R.string.preference_check_for_updates_none_found)
)
}
}
}
else -> {}
}
}
}
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
}
)
}
}
fun onOpenDeveloperOptions(context: Context) {
val launchIntent = Intent(context, DeveloperSettingsActivity::class.java)
context.startActivity(launchIntent)
}
@Composable
fun DeveloperModeDialog(onDismiss: () -> Unit, onEnable: () -> Unit) {
AlertDialog(
icon = {
Icon(
painterResource(R.drawable.icon_warning),
contentDescription = null,
)
},
title = { Text(stringResource(R.string.preference_developer_options_dialog_title)) },
text = { Text(stringResource(R.string.preference_developer_mode_about)) },
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.message_box_cancel))
}
},
confirmButton = {
TextButton(onClick = onEnable) {
Text(stringResource(R.string.message_box_accept))
}
},
onDismissRequest = onDismiss
)
}
@Composable
fun GeneralSettingsGroup() {
val context = LocalContext.current
OptionsGroup(stringResource(R.string.preference_category_testing)) {
SettingsSelectCard(
title = stringResource(R.string.preference_theme_name),
dialogTitle = stringResource(R.string.preference_theme_select),
preferenceKey = PreferenceUtils.Key.THEME,
options = linkedMapOf(
0 to stringResource(R.string.preference_theme_default),
1 to stringResource(R.string.preference_theme_light),
2 to stringResource(R.string.preference_theme_dark),
),
extraSelectBehavior = { updateTheme(context, it) }
)
SettingsCard(
title = stringResource(R.string.preference_black_background_name),
preferenceKey = PreferenceUtils.Key.BLACK_BACKGROUND
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
SettingsCard(
title = stringResource(R.string.preference_disable_styles),
preferenceKey = PreferenceUtils.Key.DISABLE_USER_THEME
)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val currentIcon by PreferenceUtils.useStringPreference(PreferenceUtils.Key.SELECTED_ICON)
val iconName = remember(currentIcon) {
IconUtils.getIconDetails(
ApplicationIcon.fromId(currentIcon ?: "default")
).nameId
}
OptionsButton(
title = stringResource(R.string.preference_set_application_icon),
onClick = {
val launchIntent = Intent(context, ApplicationIconActivity::class.java)
context.startActivity(launchIntent)
},
description = stringResource(iconName),
displayInline = true,
)
}
OptionsButton(
title = stringResource(R.string.preferences_open_file_manager),
icon = {
Icon(
painterResource(R.drawable.icon_folder),
contentDescription = null
)
},
onClick = { onOpenFileManager(context) }
)
}
}
@Composable
fun GameplaySettingsGroup() {
val context = LocalContext.current
OptionsGroup(stringResource(R.string.preference_category_gameplay)) {
SettingsCard(
title = stringResource(R.string.preference_load_automatically_name),
description = stringResource(R.string.preference_load_automatically_description),
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY,
)
SettingsSelectCard(
title = stringResource(R.string.preference_display_mode_name),
dialogTitle = stringResource(R.string.preference_display_mode_select),
preferenceKey = PreferenceUtils.Key.DISPLAY_MODE,
options = buildMap {
put(0, stringResource(R.string.preference_display_mode_default))
put(1, stringResource(R.string.preference_display_mode_legacy))
// necessary api doesn't exist on older versions of android
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
put(2, stringResource(R.string.preference_display_mode_fullscreen))
}
}
)
val currentDisplayMode by PreferenceUtils.useIntPreference(PreferenceUtils.Key.DISPLAY_MODE)
if (currentDisplayMode == 1) {
SettingsStringSelectCard(
title = stringResource(R.string.preference_custom_aspect_ratio_name),
description = stringResource(R.string.preference_custom_aspect_ratio_description),
dialogTitle = stringResource(R.string.preference_custom_aspect_ratio_select),
preferenceKey = PreferenceUtils.Key.CUSTOM_ASPECT_RATIO,
options = linkedMapOf(
"16_9" to stringResource(R.string.preference_custom_aspect_ratio_16x9),
"16_10" to stringResource(R.string.preference_custom_aspect_ratio_16x10),
"4_3" to stringResource(R.string.preference_custom_aspect_ratio_4x3),
"1_1" to stringResource(R.string.preference_custom_aspect_ratio_1x1),
)
)
}
SettingsRangeCard(
title = stringResource(R.string.preference_screen_zoom_name),
description = stringResource(R.string.preference_screen_zoom_description),
dialogTitle = stringResource(R.string.preference_screen_zoom_select),
preferenceKey = PreferenceUtils.Key.SCREEN_ZOOM,
labelSuffix = "x",
range = 25..100,
scale = 100,
step = 5,
) {
SettingsCard(
title = stringResource(R.string.preference_screen_zoom_fit_name),
preferenceKey = PreferenceUtils.Key.SCREEN_ZOOM_FIT,
asCard = false
)
}
val maxFrameRate = remember {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
context.display.supportedModes.maxOf { it.refreshRate }
} else {
@Suppress("DEPRECATION")
(context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.refreshRate
}.roundToInt()
}
SettingsFPSCard(
title = stringResource(R.string.preference_limit_framerate_name),
dialogTitle = stringResource(R.string.preference_limit_framerate_select),
preferenceKey = PreferenceUtils.Key.LIMIT_FRAME_RATE,
maxFrameRate = maxFrameRate
)
var showSafeModeDialog by remember { mutableStateOf(false) }
OptionsButton(
title = stringResource(R.string.preference_launch_safe_mode),
icon = {
Icon(
painterResource(R.drawable.icon_shield),
contentDescription = null
)
}
) {
showSafeModeDialog = true
}
if (showSafeModeDialog) {
SafeModeDialog(onDismiss = {
showSafeModeDialog = false
}) {
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)!!.apply {
// hardcode it, why not
action = Intent.ACTION_VIEW
data = "geode-launcher://main/launch?safe-mode=true".toUri()
}
context.startActivity(launchIntent)
}
}
}
}
@Composable
fun UpdateSettingsGroup(releaseViewModel: ReleaseViewModel, snackbarHostState: SnackbarHostState, onUpdate: () -> Unit) {
if (BuildConfig.GOOGLE_PLAY_BUILD) {
return
}
val updateStatus by releaseViewModel.uiState.collectAsState()
OptionsGroup(stringResource(R.string.preference_category_updater)) {
SettingsCard(
title = stringResource(R.string.preference_update_automatically_name),
preferenceKey = PreferenceUtils.Key.UPDATE_AUTOMATICALLY,
)
val lastUpdateCheck by PreferenceUtils.useLongPreference(PreferenceUtils.Key.LAST_UPDATE_CHECK_TIME)
val checkAgoString = remember(lastUpdateCheck) {
if (lastUpdateCheck != 0L) {
val now = Clock.System.now().toEpochMilliseconds()
DateUtils.getRelativeTimeSpanString(
lastUpdateCheck,
now,
DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE
)
} else {
"never"
}
}
OptionsCard(
title = {
OptionsTitle(
title = stringResource(R.string.preference_check_for_updates_button),
icon = {
Icon(
painterResource(R.drawable.icon_update),
contentDescription = null
)
},
description = stringResource(
R.string.preference_check_for_updates_description,
checkAgoString
)
.takeIf { lastUpdateCheck != 0L }
)
},
modifier = Modifier
.clickable(
onClick = {
onUpdate()
},
role = Role.Button
)
) {
UpdateIndicator(snackbarHostState, updateStatus)
}
}
}
@Composable
fun AdvancedSettingsGroup(developerModeEnabled: Boolean, snackbarHostState: SnackbarHostState) {
val context = LocalContext.current
OptionsGroup(stringResource(R.string.preference_category_developer)) {
if (developerModeEnabled) {
OptionsButton(
title = stringResource(R.string.preference_open_developer_options),
icon = {
Icon(
painterResource(R.drawable.icon_data_object),
contentDescription = null
)
},
onClick = { onOpenDeveloperOptions(context) }
)
}
OptionsButton(
title = stringResource(R.string.preferences_view_geode_logs),
icon = {
Icon(
painterResource(R.drawable.icon_description),
contentDescription = null
)
},
onClick = {
val launchIntent = Intent(context, GeodeLogsListingActivity::class.java)
context.startActivity(launchIntent)
}
)
OptionsButton(
title = stringResource(R.string.preferences_view_crashes),
icon = {
Icon(
painterResource(R.drawable.icon_bug_report),
contentDescription = null
)
},
onClick = {
val launchIntent = Intent(context, CrashLogsActivity::class.java)
context.startActivity(launchIntent)
}
)
BackupButton(snackbarHostState)
}
}
@Composable
fun AboutSettingsGroup(developerModeEnabled: Boolean) {
val context = LocalContext.current
val resources = LocalResources.current
val currentRelease by PreferenceUtils.useStringPreference(PreferenceUtils.Key.CURRENT_VERSION_TAG)
OptionsGroup(stringResource(R.string.preference_category_about)) {
var launcherButtonTapped by remember { mutableIntStateOf(0) }
var showDeveloperDialog by remember { mutableStateOf(false) }
OptionsButton(
stringResource(R.string.preference_launcher_version_name),
stringResource(
R.string.preference_launcher_version_description,
BuildConfig.VERSION_NAME
),
displayInline = true
) {
launcherButtonTapped += 1
if (launcherButtonTapped >= 7) {
if (developerModeEnabled) {
Toast.makeText(
context,
R.string.preference_enable_developer_again,
Toast.LENGTH_SHORT
)
.show()
} else {
showDeveloperDialog = true
}
launcherButtonTapped = 0
}
}
if (showDeveloperDialog) {
DeveloperModeDialog(
onDismiss = {
showDeveloperDialog = false
},
onEnable = {
PreferenceUtils.get(context)
.setBoolean(PreferenceUtils.Key.DEVELOPER_MODE, true)
showDeveloperDialog = false
}
)
}
@Suppress("SENSELESS_COMPARISON")
OptionsLabel(
stringResource(R.string.preference_loader_version_name),
stringResource(
R.string.preference_loader_version_description,
if (BuildConfig.PREBUNDLED_GEODE != null)
"${BuildConfig.PREBUNDLED_GEODE}*"
else (currentRelease ?: "unknown")
),
displayInline = true
)
val gameInstalled = remember {
GamePackageUtils.isGameInstalled(context.packageManager)
}
if (gameInstalled) {
val gameVersion = remember {
GamePackageUtils.getUnifiedVersionName(context.packageManager)
}
val gameSource = remember {
resources.getString(
when (GamePackageUtils.identifyGameSource(context.packageManager)) {
GamePackageUtils.GameSource.GOOGLE -> R.string.preference_game_source_google
GamePackageUtils.GameSource.AMAZON -> R.string.preference_game_source_amazon
else -> R.string.preference_game_source_unknown
}
)
}
OptionsLabel(
stringResource(R.string.preference_game_version_name),
stringResource(
R.string.preference_game_version_description,
gameVersion,
gameSource
),
displayInline = true
)
OptionsLabel(
stringResource(R.string.preference_architecture_name),
stringResource(
R.string.preference_architecture_description,
Build.MODEL,
LaunchUtils.applicationArchitecture
),
displayInline = true
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
onBackPressedDispatcher: OnBackPressedDispatcher?,
releaseViewModel: ReleaseViewModel = viewModel(factory = ReleaseViewModel.Factory)
) {
val resources = LocalResources.current
val snackbarHostState = remember { SnackbarHostState() }
var showUpdateInProgress by remember { mutableStateOf(false) }
if (showUpdateInProgress) {
LaunchedEffect(snackbarHostState) {
snackbarHostState.showSnackbar(
resources.getString(R.string.preference_check_for_updates_already_updating)
)
showUpdateInProgress = false
}
}
// fix theme transition by giving it the exact same animation as the top bar
val containerColor by animateColorAsState(
targetValue = MaterialTheme.colorScheme.background,
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
label = "background color"
)
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
containerColor = containerColor,
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
topBar = {
TopAppBar(
navigationIcon = {
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
Icon(
painterResource(R.drawable.icon_arrow_back),
contentDescription = stringResource(R.string.back_icon_alt)
)
}
},
title = {
Text(
stringResource(R.string.title_activity_settings),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
scrollBehavior = scrollBehavior,
colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.background),
)
},
content = { innerPadding ->
Column(
Modifier
.padding(innerPadding)
.fillMaxWidth()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
GeneralSettingsGroup()
GameplaySettingsGroup()
UpdateSettingsGroup(releaseViewModel, snackbarHostState, onUpdate = {
if (releaseViewModel.isInUpdate) {
showUpdateInProgress = true
} else {
releaseViewModel.runReleaseCheck(true)
}
})
val developerModeEnabled by PreferenceUtils.useBooleanPreference(PreferenceUtils.Key.DEVELOPER_MODE)
AdvancedSettingsGroup(developerModeEnabled, snackbarHostState)
AboutSettingsGroup(developerModeEnabled)
Spacer(Modifier.height(4.dp))
}
}
)
}
@Preview(showSystemUi = true)
@Composable
fun DefaultPreview() {
GeodeLauncherTheme {
SettingsScreen(null)
}
}