kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
				
				
				
			Move remaining 3-dot menu items to Settings (#2985)
							rodzic
							
								
									4ab588cdaa
								
							
						
					
					
						commit
						08ced48652
					
				| 
						 | 
				
			
			@ -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")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,26 +347,19 @@ fun MainScreen(
 | 
			
		|||
                    viewModel = uIViewModel,
 | 
			
		||||
                    navController = navController,
 | 
			
		||||
                    onAction = { action ->
 | 
			
		||||
                        if (action is MainMenuAction) {
 | 
			
		||||
                            when (action) {
 | 
			
		||||
                                MainMenuAction.QUICK_CHAT -> navController.navigate(ContactsRoutes.QuickChat)
 | 
			
		||||
                                else -> onAction(action)
 | 
			
		||||
                        when (action) {
 | 
			
		||||
                            is NodeMenuAction.MoreDetails -> {
 | 
			
		||||
                                navController.navigate(
 | 
			
		||||
                                    NodesRoutes.NodeDetailGraph(action.node.num),
 | 
			
		||||
                                    {
 | 
			
		||||
                                        launchSingleTop = true
 | 
			
		||||
                                        restoreState = true
 | 
			
		||||
                                    },
 | 
			
		||||
                                )
 | 
			
		||||
                            }
 | 
			
		||||
                        } else if (action is NodeMenuAction) {
 | 
			
		||||
                            when (action) {
 | 
			
		||||
                                is NodeMenuAction.MoreDetails -> {
 | 
			
		||||
                                    navController.navigate(
 | 
			
		||||
                                        NodesRoutes.NodeDetailGraph(action.node.num),
 | 
			
		||||
                                        {
 | 
			
		||||
                                            launchSingleTop = true
 | 
			
		||||
                                            restoreState = true
 | 
			
		||||
                                        },
 | 
			
		||||
                                    )
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
                                is NodeMenuAction.Share -> sharedContact = action.node
 | 
			
		||||
                                else -> {}
 | 
			
		||||
                            }
 | 
			
		||||
                            is NodeMenuAction.Share -> sharedContact = action.node
 | 
			
		||||
                            else -> {}
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 = {},
 | 
			
		||||
        ) {}
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Ładowanie…
	
		Reference in New Issue