Move remaining 3-dot menu items to Settings (#2985)

pull/2988/head
Phil Oliver 2025-09-05 15:51:09 -04:00 zatwierdzone przez GitHub
rodzic 4ab588cdaa
commit 08ced48652
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
8 zmienionych plików z 111 dodań i 149 usunięć

Wyświetl plik

@ -28,7 +28,6 @@ import android.os.Bundle
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
@ -47,7 +46,6 @@ import com.geeksville.mesh.model.BluetoothViewModel
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.navigation.DEEP_LINK_BASE_URI
import com.geeksville.mesh.ui.MainScreen
import com.geeksville.mesh.ui.common.components.MainMenuAction
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.common.theme.MODE_DYNAMIC
import com.geeksville.mesh.ui.intro.AppIntroductionScreen
@ -117,11 +115,7 @@ class MainActivity :
},
)
} else {
MainScreen(
uIViewModel = model,
bluetoothViewModel = bluetoothViewModel,
onAction = ::onMainMenuAction,
)
MainScreen(uIViewModel = model, bluetoothViewModel = bluetoothViewModel)
}
}
}
@ -204,30 +198,7 @@ class MainActivity :
return resultPendingIntent!!
}
private val createRangetestLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
it.data?.data?.let { file_uri -> model.saveRangetestCSV(file_uri) }
}
}
private fun showSettingsPage() {
createSettingsIntent().send()
}
private fun onMainMenuAction(action: MainMenuAction) {
when (action) {
MainMenuAction.EXPORT_RANGETEST -> {
val intent =
Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/csv"
putExtra(Intent.EXTRA_TITLE, "rangetest.csv")
}
createRangetestLauncher.launch(intent)
}
else -> warn("Unexpected action: $action")
}
}
}

Wyświetl plik

@ -847,7 +847,7 @@ constructor(
/** Write the persisted packet data out to a CSV file in the specified location. */
@Suppress("detekt:CyclomaticComplexMethod", "detekt:LongMethod")
fun saveRangetestCSV(uri: Uri) {
fun saveRangeTestCsv(uri: Uri) {
viewModelScope.launch(Dispatchers.Main) {
// Extract distances to this device from position messages and put (node,SNR,distance)
// in

Wyświetl plik

@ -94,7 +94,6 @@ import com.geeksville.mesh.repository.radio.MeshActivity
import com.geeksville.mesh.service.ConnectionState
import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.common.components.MainMenuAction
import com.geeksville.mesh.ui.common.components.MultipleChoiceAlertDialog
import com.geeksville.mesh.ui.common.components.ScannedQrCodeDialog
import com.geeksville.mesh.ui.common.components.SimpleAlertDialog
@ -146,7 +145,6 @@ fun MainScreen(
uIViewModel: UIViewModel = hiltViewModel(),
bluetoothViewModel: BluetoothViewModel = hiltViewModel(),
scanModel: BTScanModel = hiltViewModel(),
onAction: (MainMenuAction) -> Unit,
) {
val navController = rememberNavController()
val connectionState by uIViewModel.connectionState.collectAsStateWithLifecycle()
@ -349,12 +347,6 @@ fun MainScreen(
viewModel = uIViewModel,
navController = navController,
onAction = { action ->
if (action is MainMenuAction) {
when (action) {
MainMenuAction.QUICK_CHAT -> navController.navigate(ContactsRoutes.QuickChat)
else -> onAction(action)
}
} else if (action is NodeMenuAction) {
when (action) {
is NodeMenuAction.MoreDetails -> {
navController.navigate(
@ -369,7 +361,6 @@ fun MainScreen(
is NodeMenuAction.Share -> sharedContact = action.node
else -> {}
}
}
},
)
NavGraph(

Wyświetl plik

@ -17,27 +17,21 @@
package com.geeksville.mesh.ui.common.components
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
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.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
@ -45,6 +39,7 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavDestination.Companion.hasRoute
@ -61,7 +56,7 @@ import com.geeksville.mesh.ui.TopLevelDestination.Companion.isTopLevel
import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.debug.DebugMenuActions
import com.geeksville.mesh.ui.node.components.NodeChip
import com.geeksville.mesh.ui.settings.radio.RadioConfigMenuActions
import com.geeksville.mesh.ui.node.components.NodeMenuAction
@Suppress("CyclomaticComplexMethod")
@Composable
@ -69,7 +64,7 @@ fun MainAppBar(
modifier: Modifier = Modifier,
viewModel: UIViewModel = hiltViewModel(),
navController: NavHostController,
onAction: (Any?) -> Unit,
onAction: (NodeMenuAction) -> Unit,
) {
val backStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = backStackEntry?.destination
@ -117,13 +112,7 @@ fun MainAppBar(
actions = {
currentDestination?.let {
when {
it.isTopLevel() -> MainMenuActions(onAction)
currentDestination.hasRoute<SettingsRoutes.DebugPanel>() -> DebugMenuActions()
currentDestination.hasRoute<SettingsRoutes.Settings>() ->
RadioConfigMenuActions(viewModel = viewModel)
else -> {}
}
}
@ -144,7 +133,7 @@ private fun MainAppBar(
canNavigateUp: Boolean,
onNavigateUp: () -> Unit,
actions: @Composable () -> Unit,
onAction: (Any?) -> Unit,
onAction: (NodeMenuAction) -> Unit,
) {
TopAppBar(
title = {
@ -195,43 +184,21 @@ private fun TopBarActions(
isConnected: Boolean,
showNodeChip: Boolean,
actions: @Composable () -> Unit,
onAction: (Any?) -> Unit,
onAction: (NodeMenuAction) -> Unit,
) {
AnimatedVisibility(showNodeChip) {
ourNode?.let { NodeChip(node = it, isThisNode = true, isConnected = isConnected, onAction = onAction) }
}
actions()
}
enum class MainMenuAction(@StringRes val stringRes: Int) {
EXPORT_RANGETEST(R.string.save_rangetest),
QUICK_CHAT(R.string.quick_chat),
}
@Composable
private fun MainMenuActions(onAction: (MainMenuAction) -> Unit) {
var showMenu by remember { mutableStateOf(false) }
IconButton(onClick = { showMenu = true }) {
Icon(imageVector = Icons.Default.MoreVert, contentDescription = stringResource(R.string.overflow_menu))
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false },
modifier = Modifier.background(colorScheme.background.copy(alpha = 1f)),
) {
MainMenuAction.entries.forEach { action ->
DropdownMenuItem(
text = { Text(stringResource(id = action.stringRes)) },
onClick = {
onAction(action)
showMenu = false
},
enabled = true,
AnimatedVisibility(visible = showNodeChip, enter = fadeIn(), exit = fadeOut()) {
ourNode?.let {
NodeChip(
modifier = Modifier.padding(horizontal = 16.dp),
node = it,
isThisNode = true,
isConnected = isConnected,
onAction = onAction,
)
}
}
actions()
}
@PreviewLightDark
@ -246,7 +213,7 @@ private fun MainAppBarPreview(@PreviewParameter(BooleanProvider::class) canNavig
showNodeChip = true,
canNavigateUp = canNavigateUp,
onNavigateUp = {},
actions = { MainMenuActions(onAction = {}) },
actions = {},
) {}
}
}

Wyświetl plik

@ -25,7 +25,6 @@ import android.net.InetAddresses
import android.os.Build
import android.util.Patterns
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
@ -77,7 +76,6 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.R
import com.geeksville.mesh.android.gpsDisabled
@ -472,14 +470,12 @@ fun ConnectionsScreen(
}
Box(modifier = Modifier.fillMaxWidth().padding(8.dp)) {
Row(
Text(
text = scanStatusText.orEmpty(),
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(text = BuildConfig.VERSION_NAME, fontSize = 10.sp, textAlign = TextAlign.Start)
Text(text = scanStatusText.orEmpty(), fontSize = 10.sp, textAlign = TextAlign.End)
}
fontSize = 10.sp,
textAlign = TextAlign.End,
)
}
}
}

Wyświetl plik

@ -19,8 +19,10 @@ package com.geeksville.mesh.ui.settings
import android.app.Activity
import android.content.Intent
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
@ -30,9 +32,13 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material.icons.rounded.FormatPaint
import androidx.compose.material.icons.rounded.Language
import androidx.compose.material.icons.rounded.Memory
import androidx.compose.material.icons.rounded.Output
import androidx.compose.material.icons.rounded.WavingHand
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@ -42,8 +48,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.BuildConfig
import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile
import com.geeksville.mesh.DeviceUIProtos.Language
import com.geeksville.mesh.R
import com.geeksville.mesh.android.BuildUtils.debug
import com.geeksville.mesh.model.UIViewModel
@ -52,12 +58,15 @@ import com.geeksville.mesh.navigation.getNavRouteFrom
import com.geeksville.mesh.ui.common.components.TitledCard
import com.geeksville.mesh.ui.common.theme.MODE_DYNAMIC
import com.geeksville.mesh.ui.settings.components.SettingsItem
import com.geeksville.mesh.ui.settings.components.SettingsItemDetail
import com.geeksville.mesh.ui.settings.components.SettingsItemSwitch
import com.geeksville.mesh.ui.settings.radio.RadioConfigItemList
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import com.geeksville.mesh.ui.settings.radio.components.EditDeviceProfileDialog
import com.geeksville.mesh.ui.settings.radio.components.PacketResponseStateDialog
import com.geeksville.mesh.util.LanguageUtils
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
@ -166,7 +175,7 @@ fun SettingsScreen(
onNavigate = onNavigate,
)
TitledCard(title = stringResource(R.string.phone_settings), modifier = Modifier.padding(top = 16.dp)) {
TitledCard(title = stringResource(R.string.app_settings), modifier = Modifier.padding(top = 16.dp)) {
if (state.analyticsAvailable) {
SettingsItemSwitch(
text = stringResource(R.string.analytics_okay),
@ -214,6 +223,26 @@ fun SettingsScreen(
)
}
val exportRangeTestLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
it.data?.data?.let { uri -> uiViewModel.saveRangeTestCsv(uri) }
}
}
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, "rangetest.csv")
}
exportRangeTestLauncher.launch(intent)
}
SettingsItem(
text = stringResource(R.string.intro_show),
leadingIcon = Icons.Rounded.WavingHand,
@ -221,6 +250,46 @@ fun SettingsScreen(
) {
uiViewModel.showAppIntro()
}
AppVersionButton(excludedModulesUnlocked) { uiViewModel.unlockExcludedModules() }
}
}
}
private const val UNLOCK_CLICK_COUNT = 5 // Number of clicks required to unlock excluded modules.
private const val UNLOCKED_CLICK_COUNT = 3 // Number of clicks before we toast that modules are already unlocked.
private const val UNLOCK_TIMEOUT_SECONDS = 1 // Timeout in seconds to reset the click counter.
/** A button to display the app version. Clicking it 5 times will unlock the excluded modules. */
@Composable
private fun AppVersionButton(excludedModulesUnlocked: Boolean, onUnlockExcludedModules: () -> Unit) {
val context = LocalContext.current
var clickCount by remember { mutableIntStateOf(0) }
LaunchedEffect(clickCount) {
if (clickCount in 1..<UNLOCK_CLICK_COUNT) {
delay(UNLOCK_TIMEOUT_SECONDS.seconds)
clickCount = 0
}
}
SettingsItemDetail(
text = stringResource(R.string.app_version),
icon = Icons.Rounded.Memory,
trailingText = BuildConfig.VERSION_NAME,
) {
clickCount = clickCount.inc().coerceIn(0, UNLOCK_CLICK_COUNT)
when {
clickCount == UNLOCKED_CLICK_COUNT && excludedModulesUnlocked -> {
clickCount = 0
Toast.makeText(context, context.getString(R.string.modules_already_unlocked), Toast.LENGTH_LONG).show()
}
clickCount == UNLOCK_CLICK_COUNT -> {
clickCount = 0
onUnlockExcludedModules()
Toast.makeText(context, context.getString(R.string.modules_unlocked), Toast.LENGTH_LONG).show()
}
}
}
}

Wyświetl plik

@ -17,7 +17,6 @@
package com.geeksville.mesh.ui.settings.radio
import android.widget.Toast
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
@ -25,24 +24,19 @@ import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Upload
import androidx.compose.material.icons.rounded.BugReport
import androidx.compose.material.icons.rounded.CleaningServices
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.geeksville.mesh.R
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.navigation.AdminRoute
import com.geeksville.mesh.navigation.ConfigRoute
import com.geeksville.mesh.navigation.ModuleRoute
@ -53,8 +47,6 @@ import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.common.theme.StatusColors.StatusRed
import com.geeksville.mesh.ui.settings.components.SettingsItem
import com.geeksville.mesh.ui.settings.radio.components.WarningDialog
import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
@ -168,32 +160,6 @@ fun RadioConfigItemList(
}
}
private const val UNLOCK_CLICK_COUNT = 5 // Number of clicks required to unlock excluded modules.
private const val UNLOCK_TIMEOUT_SECONDS = 3 // Timeout in seconds to reset the click counter.
@Composable
fun RadioConfigMenuActions(modifier: Modifier = Modifier, viewModel: UIViewModel = hiltViewModel()) {
val context = LocalContext.current
var counter by remember { mutableIntStateOf(0) }
LaunchedEffect(counter) {
if (counter > 0 && counter < UNLOCK_CLICK_COUNT) {
delay(UNLOCK_TIMEOUT_SECONDS.seconds)
counter = 0
}
}
IconButton(
enabled = counter < UNLOCK_CLICK_COUNT,
onClick = {
counter++
if (counter == UNLOCK_CLICK_COUNT) {
viewModel.unlockExcludedModules()
Toast.makeText(context, context.getString(R.string.modules_unlocked), Toast.LENGTH_LONG).show()
}
},
modifier = modifier,
) {}
}
@Preview(showBackground = true)
@Composable
private fun RadioSettingsScreenPreview() = AppTheme {

Wyświetl plik

@ -653,6 +653,7 @@
<string name="export_keys">Export Keys</string>
<string name="export_keys_confirmation">Exports public and private keys to a file. Please store somewhere securely.</string>
<string name="modules_unlocked">Modules unlocked</string>
<string name="modules_already_unlocked">Modules already unlocked</string>
<string name="remote">Remote</string>
<string name="node_count_template">(%1$d online / %2$d total)</string>
<string name="react">React</string>
@ -801,7 +802,8 @@
<string name="url_template">URL Template</string>
<string name="url_template_hint" translatable="false">https://a.tile.openstreetmap.org/{z}/{x}/{y}.png</string>
<string name="track_point">track point</string>
<string name="phone_settings">Phone Settings</string>
<string name="app_settings">App</string>
<string name="app_version">Version</string>
<string name="channel_features">Channel Features</string>
<string name="location_sharing">Location Sharing</string>
<string name="periodic_position_broadcast">Periodic position broadcast</string>