forked from geode-sdk/android-launcher
move settings
This commit is contained in:
6 files changed
+12
-11
No files matched your search
@@ -0,0 +1,395 @@
|
||||
package com.geode.launcher.preferences
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedDispatcher
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
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
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
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.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.graphics.Color
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.geode.launcher.R
|
||||
import com.geode.launcher.log.LogLine
|
||||
import com.geode.launcher.log.LogPriority
|
||||
import com.geode.launcher.log.LogViewModel
|
||||
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.utils.PreferenceUtils
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ApplicationLogsActivity : 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)
|
||||
|
||||
CompositionLocalProvider(LocalTheme provides theme) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
ApplicationLogsScreen(onBackPressedDispatcher)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun getLogColor(priority: LogPriority): Color {
|
||||
val isDark = LocalTheme.current != Theme.LIGHT
|
||||
|
||||
// i just copied these from my ide tbh
|
||||
return if (isDark) {
|
||||
when (priority) {
|
||||
LogPriority.ERROR -> Color(0xff_df_51_51)
|
||||
LogPriority.WARN -> Color(0xff_bc_b5_00)
|
||||
LogPriority.FATAL -> Color(0xff_b8_b6_fc)
|
||||
LogPriority.DEBUG -> Color(0xff_aa_aa_aa)
|
||||
LogPriority.VERBOSE -> Color(0xff_77_77_77)
|
||||
else -> Color.Unspecified
|
||||
}
|
||||
} else {
|
||||
when (priority) {
|
||||
LogPriority.ERROR -> Color(0xff_df_51_51)
|
||||
LogPriority.WARN -> Color(0xff_ea_b7_00)
|
||||
LogPriority.FATAL -> Color(0xff_a1_42_f4)
|
||||
LogPriority.DEBUG -> Color(0xff_77_77_77)
|
||||
LogPriority.VERBOSE -> Color(0xff_aa_aa_aa)
|
||||
else -> Color.Unspecified
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SelectLogLevelDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onSelect: (LogPriority) -> Unit,
|
||||
initialValue: LogPriority
|
||||
) {
|
||||
SelectDialog(
|
||||
title = stringResource(R.string.application_logs_level_dialog_name),
|
||||
onDismissRequest = onDismissRequest,
|
||||
onSelect = { onSelect(it) },
|
||||
initialValue = initialValue,
|
||||
toLabel = {
|
||||
// i'm lazy
|
||||
it.name.lowercase().replaceFirstChar { c -> c.uppercaseChar() }
|
||||
},
|
||||
optionsCount = LogPriority.FATAL downTo LogPriority.VERBOSE
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LogCard(logLine: LogLine, modifier: Modifier = Modifier) {
|
||||
val haptics = LocalHapticFeedback.current
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.padding(8.dp)
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onLongPress = {
|
||||
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
clipboardManager.setText(AnnotatedString(logLine.asSimpleString))
|
||||
}
|
||||
)
|
||||
},
|
||||
) {
|
||||
Text(
|
||||
"[${logLine.priority.toChar()}] ${logLine.formattedTime}",
|
||||
style = Typography.labelSmall,
|
||||
color = getLogColor(logLine.priority),
|
||||
fontFamily = FontFamily.Monospace
|
||||
)
|
||||
|
||||
Text(
|
||||
"[${logLine.tag}]: ${logLine.messageTrimmed}",
|
||||
fontFamily = FontFamily.Monospace,
|
||||
style = Typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ApplicationLogsScreen(
|
||||
onBackPressedDispatcher: OnBackPressedDispatcher?,
|
||||
logViewModel: LogViewModel = viewModel()
|
||||
) {
|
||||
val logLines = logViewModel.lineState
|
||||
val isLoading by logViewModel.isLoading.collectAsState()
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
var showMoreOptions by remember { mutableStateOf(false) }
|
||||
val saveLauncher = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { uri ->
|
||||
if (uri != null) {
|
||||
coroutineScope.launch {
|
||||
val logs = logViewModel.getLogData()
|
||||
|
||||
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
|
||||
outputStream.bufferedWriter().use { writer ->
|
||||
writer.write(logs)
|
||||
}
|
||||
}
|
||||
|
||||
Toast.makeText(context, R.string.application_logs_export_completed, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(logLines.size) {
|
||||
listState.scrollToItem(logLines.size)
|
||||
}
|
||||
|
||||
var showLogLevelSelect by remember { mutableStateOf(false) }
|
||||
if (showLogLevelSelect) {
|
||||
SelectLogLevelDialog(
|
||||
onDismissRequest = { showLogLevelSelect = false },
|
||||
onSelect = {
|
||||
logViewModel.logLevel = it
|
||||
showLogLevelSelect = false
|
||||
},
|
||||
initialValue = logViewModel.logLevel
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
topBar = {
|
||||
Column {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { onBackPressedDispatcher?.onBackPressed() }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back_icon_alt)
|
||||
)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
coroutineScope.launch {
|
||||
val data = logViewModel.getLogData()
|
||||
|
||||
val sendIntent = Intent(Intent.ACTION_SEND).apply {
|
||||
putExtra(Intent.EXTRA_TEXT, data)
|
||||
type = "text/plain"
|
||||
}
|
||||
|
||||
val shareIntent = Intent.createChooser(sendIntent, null)
|
||||
|
||||
context.startActivity(shareIntent)
|
||||
}
|
||||
}, enabled = !logLines.isEmpty()) {
|
||||
Icon(
|
||||
Icons.Filled.Share,
|
||||
contentDescription = stringResource(R.string.application_logs_share)
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { showMoreOptions = !showMoreOptions }) {
|
||||
Icon(
|
||||
Icons.Filled.MoreVert,
|
||||
contentDescription = stringResource(R.string.application_logs_more)
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = showMoreOptions,
|
||||
onDismissRequest = { showMoreOptions = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_bug_report),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
if (logViewModel.filterCrashes) {
|
||||
Icon(
|
||||
Icons.Filled.Check,
|
||||
contentDescription = stringResource(R.string.application_logs_crash_only_enabled_alt)
|
||||
)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.application_logs_crash_only))
|
||||
}, onClick = {
|
||||
logViewModel.toggleCrashBuffer()
|
||||
showMoreOptions = false
|
||||
}
|
||||
)
|
||||
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_filter_list),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.application_logs_level))
|
||||
},
|
||||
onClick = {
|
||||
showLogLevelSelect = true
|
||||
showMoreOptions = false
|
||||
}
|
||||
)
|
||||
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_save),
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.application_logs_export))
|
||||
},
|
||||
onClick = {
|
||||
saveLauncher.launch(context.getString(R.string.application_logs_default_filename))
|
||||
showMoreOptions = false
|
||||
},
|
||||
enabled = !logLines.isEmpty()
|
||||
)
|
||||
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
painterResource(R.drawable.icon_delete),
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.application_logs_delete))
|
||||
}, onClick = {
|
||||
logViewModel.clearLogs()
|
||||
showMoreOptions = false
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.title_activity_application_logs),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
|
||||
if (isLoading) {
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
LazyColumn(
|
||||
Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.SpaceEvenly,
|
||||
state = listState
|
||||
) {
|
||||
if (!isLoading && logLines.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
stringResource(R.string.application_logs_no_logs),
|
||||
textAlign = TextAlign.Center,
|
||||
style = Typography.bodyLarge,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items(
|
||||
logLines
|
||||
) { line ->
|
||||
LogCard(line)
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showSystemUi = true)
|
||||
@Composable
|
||||
fun LogsPreview() {
|
||||
GeodeLauncherTheme {
|
||||
ApplicationLogsScreen(null)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
package com.geode.launcher.preferences
|
||||
|
||||
import android.app.UiModeManager
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.DocumentsContract
|
||||
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.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
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.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
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.lifecycle.viewmodel.compose.viewModel
|
||||
import com.geode.launcher.BuildConfig
|
||||
import com.geode.launcher.R
|
||||
import com.geode.launcher.UserDirectoryProvider
|
||||
import com.geode.launcher.updater.ReleaseViewModel
|
||||
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.utils.LaunchUtils
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
import java.net.ConnectException
|
||||
import java.net.UnknownHostException
|
||||
|
||||
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)
|
||||
|
||||
CompositionLocalProvider(LocalTheme provides theme) {
|
||||
GeodeLauncherTheme(theme = theme, blackBackground = backgroundOption) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
SettingsScreen(
|
||||
onBackPressedDispatcher = onBackPressedDispatcher
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onOpenFolder(context: Context) {
|
||||
val file = LaunchUtils.getBaseDirectory(context)
|
||||
val clipboardManager =
|
||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboardManager.setPrimaryClip(
|
||||
ClipData.newPlainText(
|
||||
context.getString(R.string.export_folder_tag),
|
||||
file.path
|
||||
)
|
||||
)
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.text_copied),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
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 (e: ActivityNotFoundException) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getText(R.string.no_activity_found), Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpdateIndicator(
|
||||
snackbarHostState: SnackbarHostState,
|
||||
updateStatus: ReleaseViewModel.ReleaseUIState
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
var enablePopup by remember { mutableStateOf(false) }
|
||||
|
||||
when (updateStatus) {
|
||||
is ReleaseViewModel.ReleaseUIState.InUpdateCheck -> {
|
||||
enablePopup = true
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
is ReleaseViewModel.ReleaseUIState.InDownload -> {
|
||||
// is this the ideal design? idk
|
||||
enablePopup = true
|
||||
val progress = updateStatus.downloaded / updateStatus.outOf.toDouble()
|
||||
|
||||
CircularProgressIndicator(
|
||||
progress = { progress.toFloat() },
|
||||
)
|
||||
}
|
||||
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 ->
|
||||
context.getString(R.string.release_fetch_no_internet)
|
||||
else -> context.getString(R.string.preference_check_for_updates_failed)
|
||||
}
|
||||
|
||||
snackbarHostState.showSnackbar(message)
|
||||
}
|
||||
is ReleaseViewModel.ReleaseUIState.Finished -> {
|
||||
if (updateStatus.hasUpdated) {
|
||||
snackbarHostState.showSnackbar(
|
||||
context.getString(R.string.preference_check_for_updates_success)
|
||||
)
|
||||
} else {
|
||||
snackbarHostState.showSnackbar(
|
||||
context.getString(R.string.preference_check_for_updates_none_found)
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun displayOptionToKey(option: Int): String {
|
||||
return when (option) {
|
||||
1 -> stringResource(R.string.preference_display_mode_legacy)
|
||||
2 -> stringResource(R.string.preference_display_mode_fullscreen)
|
||||
else -> stringResource(R.string.preference_display_mode_default)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun releaseChannelToKey(option: Int): String = when (option) {
|
||||
1 -> stringResource(R.string.preference_release_channel_beta)
|
||||
2 -> stringResource(R.string.preference_release_channel_nightly)
|
||||
else -> stringResource(R.string.preference_release_channel_stable)
|
||||
}
|
||||
|
||||
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 onOpenLogs(context: Context) {
|
||||
val launchIntent = Intent(context, ApplicationLogsActivity::class.java)
|
||||
context.startActivity(launchIntent)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
onBackPressedDispatcher: OnBackPressedDispatcher?,
|
||||
releaseViewModel: ReleaseViewModel = viewModel(factory = ReleaseViewModel.Factory)
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val currentRelease by PreferenceUtils.useStringPreference(PreferenceUtils.Key.CURRENT_VERSION_TAG)
|
||||
val updateStatus by releaseViewModel.uiState.collectAsState()
|
||||
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
var showUpdateInProgress by remember { mutableStateOf(false) }
|
||||
|
||||
if (showUpdateInProgress) {
|
||||
LaunchedEffect(snackbarHostState) {
|
||||
snackbarHostState.showSnackbar(
|
||||
context.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(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = context.getString(R.string.back_icon_alt)
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
context.getString(R.string.title_activity_settings),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
)
|
||||
},
|
||||
content = { innerPadding ->
|
||||
Column(
|
||||
Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
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(
|
||||
title = stringResource(R.string.preference_black_background_name),
|
||||
preferenceKey = PreferenceUtils.Key.BLACK_BACKGROUND
|
||||
)
|
||||
SettingsCard(
|
||||
title = context.getString(R.string.preference_load_automatically_name),
|
||||
description = context.getString(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),
|
||||
// necessary api doesn't exist on older versions of android
|
||||
maxVal = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) 2 else 1,
|
||||
preferenceKey = PreferenceUtils.Key.DISPLAY_MODE,
|
||||
toLabel = { displayOptionToKey(it) }
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
SettingsCard(
|
||||
title = context.getString(R.string.preference_force_hrr),
|
||||
preferenceKey = PreferenceUtils.Key.FORCE_HRR,
|
||||
)
|
||||
}
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preferences_open_file_manager),
|
||||
onClick = { onOpenFileManager(context) }
|
||||
)
|
||||
}
|
||||
|
||||
OptionsGroup(context.getString(R.string.preference_category_updater)) {
|
||||
SettingsCard(
|
||||
title = context.getString(R.string.preference_update_automatically_name),
|
||||
preferenceKey = PreferenceUtils.Key.UPDATE_AUTOMATICALLY,
|
||||
)
|
||||
SettingsSelectCard(
|
||||
title = stringResource(R.string.preference_release_channel_tag_name),
|
||||
dialogTitle = stringResource(R.string.preference_release_channel_select),
|
||||
maxVal = 2,
|
||||
preferenceKey = PreferenceUtils.Key.RELEASE_CHANNEL_TAG,
|
||||
toLabel = { releaseChannelToKey(it) }
|
||||
)
|
||||
OptionsCard(
|
||||
title = {
|
||||
OptionsTitle(
|
||||
title = stringResource(R.string.preference_check_for_updates_button),
|
||||
description = stringResource(
|
||||
R.string.preference_check_for_updates_description,
|
||||
currentRelease ?: "unknown"
|
||||
)
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
onClick = {
|
||||
if (releaseViewModel.isInUpdate) {
|
||||
showUpdateInProgress = true
|
||||
} else {
|
||||
releaseViewModel.runReleaseCheck(true)
|
||||
}
|
||||
},
|
||||
role = Role.Button
|
||||
)
|
||||
) {
|
||||
UpdateIndicator(snackbarHostState, updateStatus)
|
||||
}
|
||||
}
|
||||
|
||||
OptionsGroup(stringResource(R.string.preference_category_developer)) {
|
||||
SettingsStringCard(
|
||||
title = stringResource(R.string.preference_launch_arguments_name),
|
||||
dialogTitle = stringResource(R.string.preference_launch_arguments_set_title),
|
||||
preferenceKey = PreferenceUtils.Key.LAUNCH_ARGUMENTS,
|
||||
filterInput = { it.filter { c ->
|
||||
// if only there was a better way to define this!
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890!@#$%^&*(){}<>[]?:;'\"~`-_+=\\| ".contains(c)
|
||||
}}
|
||||
)
|
||||
OptionsButton(
|
||||
title = context.getString(R.string.preferences_copy_external_button),
|
||||
description = LaunchUtils.getBaseDirectory(context).path,
|
||||
onClick = { onOpenFolder(context) }
|
||||
)
|
||||
OptionsButton(
|
||||
title = stringResource(R.string.preferences_view_logs),
|
||||
onClick = { onOpenLogs(context) }
|
||||
)
|
||||
SettingsCard(
|
||||
title = context.getString(R.string.preference_enable_redesign),
|
||||
preferenceKey = PreferenceUtils.Key.ENABLE_REDESIGN,
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
stringResource(R.string.preference_launcher_version, BuildConfig.VERSION_NAME),
|
||||
style = Typography.labelSmall,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showSystemUi = true)
|
||||
@Composable
|
||||
fun DefaultPreview() {
|
||||
GeodeLauncherTheme {
|
||||
SettingsScreen(null)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
package com.geode.launcher.preferences
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.foundation.selection.toggleable
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.geode.launcher.R
|
||||
import com.geode.launcher.ui.theme.GeodeLauncherTheme
|
||||
import com.geode.launcher.ui.theme.Typography
|
||||
import com.geode.launcher.utils.PreferenceUtils
|
||||
|
||||
|
||||
fun toggleSetting(context: Context, preferenceKey: PreferenceUtils.Key): Boolean {
|
||||
val preferences = PreferenceUtils.get(context)
|
||||
|
||||
return preferences.toggleBoolean(preferenceKey)
|
||||
}
|
||||
|
||||
fun getSetting(context: Context, preferenceKey: PreferenceUtils.Key): Boolean {
|
||||
val preferences = PreferenceUtils.get(context)
|
||||
|
||||
return preferences.getBoolean(preferenceKey)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OptionsGroup(title: String, content: @Composable () -> Unit) {
|
||||
Column {
|
||||
Text(
|
||||
title,
|
||||
style = Typography.titleMedium,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
)
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@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 = 0..maxVal
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsStringCard(
|
||||
title: String,
|
||||
dialogTitle: String,
|
||||
preferenceKey: PreferenceUtils.Key,
|
||||
filterInput: ((String) -> String)? = null
|
||||
) {
|
||||
var preferenceValue by PreferenceUtils.useStringPreference(preferenceKey)
|
||||
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
|
||||
OptionsCard(
|
||||
title = {
|
||||
OptionsTitle(title = title, description = preferenceValue)
|
||||
},
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
onClick = {
|
||||
showDialog = true
|
||||
},
|
||||
role = Role.Button
|
||||
)
|
||||
) { }
|
||||
|
||||
if (showDialog) {
|
||||
StringDialog(
|
||||
title = dialogTitle,
|
||||
onDismissRequest = { showDialog = false },
|
||||
onSelect = {
|
||||
preferenceValue = it
|
||||
showDialog = false
|
||||
},
|
||||
initialValue = preferenceValue ?: "",
|
||||
filterInput = filterInput
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StringDialog(
|
||||
title: String,
|
||||
onDismissRequest: () -> Unit,
|
||||
onSelect: (String) -> Unit,
|
||||
initialValue: String,
|
||||
filterInput: ((String) -> String)? = null
|
||||
) {
|
||||
var enteredValue by remember { mutableStateOf(initialValue) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { onDismissRequest() },
|
||||
title = {
|
||||
Text(title)
|
||||
},
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
value = enteredValue,
|
||||
onValueChange = {
|
||||
enteredValue = filterInput?.invoke(it) ?: it
|
||||
},
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Ascii,
|
||||
autoCorrect = false
|
||||
),
|
||||
trailingIcon = {
|
||||
if (enteredValue.isNotEmpty()) {
|
||||
IconButton(onClick = { enteredValue = "" }) {
|
||||
Icon(
|
||||
Icons.Filled.Clear,
|
||||
contentDescription = stringResource(R.string.preference_text_clear)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { onSelect(enteredValue) }) {
|
||||
Text(stringResource(R.string.message_box_accept))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(stringResource(R.string.message_box_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> SelectDialog(
|
||||
title: String,
|
||||
onDismissRequest: () -> Unit,
|
||||
onSelect: (T) -> Unit,
|
||||
initialValue: T,
|
||||
toLabel: @Composable (T) -> String,
|
||||
optionsCount: Iterable<T>,
|
||||
) {
|
||||
var selectedValue by remember { mutableStateOf(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
|
||||
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
|
||||
fun OptionsButton(title: String, description: String? = null, onClick: () -> Unit) {
|
||||
OptionsCard(
|
||||
title = {
|
||||
OptionsTitle(
|
||||
title = title,
|
||||
description = description
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.clickable(onClick = onClick, role = Role.Button)
|
||||
) { }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsCard(title: String, description: String? = null, preferenceKey: PreferenceUtils.Key) {
|
||||
val context = LocalContext.current
|
||||
val settingEnabled = remember {
|
||||
mutableStateOf(getSetting(context, preferenceKey))
|
||||
}
|
||||
|
||||
OptionsCard(
|
||||
title = {
|
||||
OptionsTitle(
|
||||
Modifier.fillMaxWidth(0.75f),
|
||||
title = title,
|
||||
description = description
|
||||
)
|
||||
},
|
||||
modifier = Modifier.toggleable(
|
||||
value = settingEnabled.value,
|
||||
onValueChange = { settingEnabled.value = toggleSetting(context, preferenceKey) },
|
||||
role = Role.Switch,
|
||||
)
|
||||
) {
|
||||
Switch(checked = settingEnabled.value, onCheckedChange = null)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OptionsTitle(modifier: Modifier = Modifier, title: String, description: String? = null) {
|
||||
Column(
|
||||
modifier,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(title)
|
||||
if (!description.isNullOrEmpty()) {
|
||||
Text(
|
||||
description,
|
||||
style = Typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OptionsCard(modifier: Modifier = Modifier, title: @Composable () -> Unit, content: @Composable () -> Unit) {
|
||||
Row(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.height(64.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
Arrangement.SpaceBetween,
|
||||
Alignment.CenterVertically,
|
||||
) {
|
||||
title()
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun OptionsCardPreview() {
|
||||
GeodeLauncherTheme {
|
||||
OptionsGroup(title = "Preview Group") {
|
||||
SettingsCard(
|
||||
title = "Load files from /test",
|
||||
description = "Very long testing description goes here. It is incredibly long, it should wrap onto a new line.",
|
||||
preferenceKey = PreferenceUtils.Key.BLACK_BACKGROUND
|
||||
)
|
||||
SettingsCard(
|
||||
title = "Testing option 2",
|
||||
preferenceKey = PreferenceUtils.Key.LOAD_AUTOMATICALLY
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user