Start migration away from global top app bar (#3132)

This commit is contained in:
Phil Oliver 2025-09-17 18:38:22 -04:00 committed by GitHub
parent e4bfce0989
commit fed3ebbd36
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 685 additions and 466 deletions

View file

@ -37,6 +37,7 @@ import androidx.compose.material.icons.rounded.LocationOn
import androidx.compose.material.icons.rounded.Memory
import androidx.compose.material.icons.rounded.Output
import androidx.compose.material.icons.rounded.WavingHand
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@ -59,8 +60,10 @@ import com.geeksville.mesh.android.gpsDisabled
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.navigation.Route
import com.geeksville.mesh.navigation.getNavRouteFrom
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.common.components.TitledCard
import com.geeksville.mesh.ui.common.theme.MODE_DYNAMIC
import com.geeksville.mesh.ui.node.components.NodeMenuAction
import com.geeksville.mesh.ui.settings.components.SettingsItem
import com.geeksville.mesh.ui.settings.components.SettingsItemDetail
import com.geeksville.mesh.ui.settings.components.SettingsItemSwitch
@ -83,12 +86,13 @@ import kotlin.time.Duration.Companion.seconds
fun SettingsScreen(
viewModel: RadioConfigViewModel = hiltViewModel(),
uiViewModel: UIViewModel = hiltViewModel(),
onClickNodeChip: (Int) -> Unit = {},
onNavigate: (Route) -> Unit = {},
) {
uiViewModel.setTitle(stringResource(R.string.bottom_nav_settings))
val excludedModulesUnlocked by uiViewModel.excludedModulesUnlocked.collectAsStateWithLifecycle()
val localConfig by uiViewModel.localConfig.collectAsStateWithLifecycle()
val ourNode by uiViewModel.ourNodeInfo.collectAsStateWithLifecycle()
val isConnected by uiViewModel.isConnectedStateFlow.collectAsStateWithLifecycle(false)
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
var isWaiting by remember { mutableStateOf(false) }
@ -162,163 +166,183 @@ fun SettingsScreen(
)
}
Column(modifier = Modifier.verticalScroll(rememberScrollState()).padding(16.dp)) {
RadioConfigItemList(
state = state,
isManaged = localConfig.security.isManaged,
excludedModulesUnlocked = excludedModulesUnlocked,
onRouteClick = { route ->
isWaiting = true
viewModel.setResponseStateLoading(route)
},
onImport = {
viewModel.clearPacketResponse()
deviceProfile = null
val intent =
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/*"
Scaffold(
topBar = {
MainAppBar(
title = stringResource(R.string.bottom_nav_settings),
ourNode = ourNode,
isConnected = isConnected,
showNodeChip = ourNode != null && isConnected,
canNavigateUp = false,
onNavigateUp = {},
actions = {},
onAction = { action ->
when (action) {
is NodeMenuAction.MoreDetails -> onClickNodeChip(action.node.num)
else -> {}
}
importConfigLauncher.launch(intent)
},
onExport = {
viewModel.clearPacketResponse()
deviceProfile = null
showEditDeviceProfileDialog = true
},
onNavigate = onNavigate,
)
},
)
},
) { paddingValues ->
Column(modifier = Modifier.verticalScroll(rememberScrollState()).padding(paddingValues).padding(16.dp)) {
RadioConfigItemList(
state = state,
isManaged = localConfig.security.isManaged,
excludedModulesUnlocked = excludedModulesUnlocked,
onRouteClick = { route ->
isWaiting = true
viewModel.setResponseStateLoading(route)
},
onImport = {
viewModel.clearPacketResponse()
deviceProfile = null
val intent =
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/*"
}
importConfigLauncher.launch(intent)
},
onExport = {
viewModel.clearPacketResponse()
deviceProfile = null
showEditDeviceProfileDialog = true
},
onNavigate = onNavigate,
)
val context = LocalContext.current
val context = LocalContext.current
TitledCard(title = stringResource(R.string.app_settings), modifier = Modifier.padding(top = 16.dp)) {
if (state.analyticsAvailable) {
SettingsItemSwitch(
text = stringResource(R.string.analytics_okay),
checked = state.analyticsEnabled,
leadingIcon = Icons.Default.BugReport,
onClick = { viewModel.toggleAnalytics() },
)
}
TitledCard(title = stringResource(R.string.app_settings), modifier = Modifier.padding(top = 16.dp)) {
if (state.analyticsAvailable) {
SettingsItemSwitch(
text = stringResource(R.string.analytics_okay),
checked = state.analyticsEnabled,
leadingIcon = Icons.Default.BugReport,
onClick = { viewModel.toggleAnalytics() },
)
}
val locationPermissionsState =
rememberMultiplePermissionsState(permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION))
val isGpsDisabled = context.gpsDisabled()
val provideLocation by uiViewModel.provideLocation.collectAsState(false)
val locationPermissionsState =
rememberMultiplePermissionsState(permissions = listOf(Manifest.permission.ACCESS_FINE_LOCATION))
val isGpsDisabled = context.gpsDisabled()
val provideLocation by uiViewModel.provideLocation.collectAsState(false)
LaunchedEffect(provideLocation, locationPermissionsState.allPermissionsGranted, isGpsDisabled) {
if (provideLocation) {
if (locationPermissionsState.allPermissionsGranted) {
if (!isGpsDisabled) {
uiViewModel.meshService?.startProvideLocation()
LaunchedEffect(provideLocation, locationPermissionsState.allPermissionsGranted, isGpsDisabled) {
if (provideLocation) {
if (locationPermissionsState.allPermissionsGranted) {
if (!isGpsDisabled) {
uiViewModel.meshService?.startProvideLocation()
} else {
uiViewModel.showSnackBar(context.getString(R.string.location_disabled))
}
} else {
uiViewModel.showSnackBar(context.getString(R.string.location_disabled))
// Request permissions if not granted and user wants to provide location
locationPermissionsState.launchMultiplePermissionRequest()
}
} else {
// Request permissions if not granted and user wants to provide location
locationPermissionsState.launchMultiplePermissionRequest()
}
} else {
uiViewModel.meshService?.stopProvideLocation()
}
}
SettingsItemSwitch(
text = stringResource(R.string.provide_location_to_mesh),
leadingIcon = Icons.Rounded.LocationOn,
enabled = !isGpsDisabled,
checked = provideLocation,
) {
uiViewModel.setProvideLocation(!provideLocation)
}
val languageTags = remember { LanguageUtils.getLanguageTags(context) }
SettingsItem(
text = stringResource(R.string.preferences_language),
leadingIcon = Icons.Rounded.Language,
trailingIcon = null,
) {
val lang = LanguageUtils.getLocale()
debug("Lang from prefs: $lang")
val langMap = languageTags.mapValues { (_, value) -> { LanguageUtils.setLocale(value) } }
uiViewModel.showAlert(
title = context.getString(R.string.preferences_language),
message = "",
choices = langMap,
)
}
val themeMap = remember {
mapOf(
context.getString(R.string.dynamic) to MODE_DYNAMIC,
context.getString(R.string.theme_light) to AppCompatDelegate.MODE_NIGHT_NO,
context.getString(R.string.theme_dark) to AppCompatDelegate.MODE_NIGHT_YES,
context.getString(R.string.theme_system) to AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
)
}
SettingsItem(
text = stringResource(R.string.theme),
leadingIcon = Icons.Rounded.FormatPaint,
trailingIcon = null,
) {
uiViewModel.showAlert(
title = context.getString(R.string.choose_theme),
message = "",
choices = themeMap.mapValues { (_, value) -> { uiViewModel.setTheme(value) } },
)
}
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val exportRangeTestLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
it.data?.data?.let { uri -> uiViewModel.saveDataCsv(uri) }
uiViewModel.meshService?.stopProvideLocation()
}
}
SettingsItem(
text = stringResource(R.string.save_rangetest),
leadingIcon = Icons.Rounded.Output,
trailingIcon = null,
) {
val intent =
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/csv"
putExtra(Intent.EXTRA_TITLE, "Meshtastic_rangetest_$timestamp.csv")
}
exportRangeTestLauncher.launch(intent)
}
val exportDataLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
it.data?.data?.let { uri -> uiViewModel.saveDataCsv(uri) }
}
SettingsItemSwitch(
text = stringResource(R.string.provide_location_to_mesh),
leadingIcon = Icons.Rounded.LocationOn,
enabled = !isGpsDisabled,
checked = provideLocation,
) {
uiViewModel.setProvideLocation(!provideLocation)
}
SettingsItem(
text = stringResource(R.string.export_data_csv),
leadingIcon = Icons.Rounded.Output,
trailingIcon = null,
) {
val intent =
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/csv"
putExtra(Intent.EXTRA_TITLE, "Meshtastic_datalog_$timestamp.csv")
val languageTags = remember { LanguageUtils.getLanguageTags(context) }
SettingsItem(
text = stringResource(R.string.preferences_language),
leadingIcon = Icons.Rounded.Language,
trailingIcon = null,
) {
val lang = LanguageUtils.getLocale()
debug("Lang from prefs: $lang")
val langMap = languageTags.mapValues { (_, value) -> { LanguageUtils.setLocale(value) } }
uiViewModel.showAlert(
title = context.getString(R.string.preferences_language),
message = "",
choices = langMap,
)
}
val themeMap = remember {
mapOf(
context.getString(R.string.dynamic) to MODE_DYNAMIC,
context.getString(R.string.theme_light) to AppCompatDelegate.MODE_NIGHT_NO,
context.getString(R.string.theme_dark) to AppCompatDelegate.MODE_NIGHT_YES,
context.getString(R.string.theme_system) to AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
)
}
SettingsItem(
text = stringResource(R.string.theme),
leadingIcon = Icons.Rounded.FormatPaint,
trailingIcon = null,
) {
uiViewModel.showAlert(
title = context.getString(R.string.choose_theme),
message = "",
choices = themeMap.mapValues { (_, value) -> { uiViewModel.setTheme(value) } },
)
}
val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val exportRangeTestLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
it.data?.data?.let { uri -> uiViewModel.saveDataCsv(uri) }
}
}
exportDataLauncher.launch(intent)
}
SettingsItem(
text = stringResource(R.string.save_rangetest),
leadingIcon = Icons.Rounded.Output,
trailingIcon = null,
) {
val intent =
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/csv"
putExtra(Intent.EXTRA_TITLE, "Meshtastic_rangetest_$timestamp.csv")
}
exportRangeTestLauncher.launch(intent)
}
SettingsItem(
text = stringResource(R.string.intro_show),
leadingIcon = Icons.Rounded.WavingHand,
trailingIcon = null,
) {
uiViewModel.showAppIntro()
}
val exportDataLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
it.data?.data?.let { uri -> uiViewModel.saveDataCsv(uri) }
}
}
SettingsItem(
text = stringResource(R.string.export_data_csv),
leadingIcon = Icons.Rounded.Output,
trailingIcon = null,
) {
val intent =
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/csv"
putExtra(Intent.EXTRA_TITLE, "Meshtastic_datalog_$timestamp.csv")
}
exportDataLauncher.launch(intent)
}
AppVersionButton(excludedModulesUnlocked) { uiViewModel.unlockExcludedModules() }
SettingsItem(
text = stringResource(R.string.intro_show),
leadingIcon = Icons.Rounded.WavingHand,
trailingIcon = null,
) {
uiViewModel.showAppIntro()
}
AppVersionButton(excludedModulesUnlocked) { uiViewModel.unlockExcludedModules() }
}
}
}
}