Move app intro to :feature:intro (#3348)

This commit is contained in:
Phil Oliver 2025-10-04 21:15:55 -04:00 committed by GitHub
parent 8562073d44
commit dcbf664f38
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 50 additions and 12 deletions

View file

@ -41,12 +41,12 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.MainScreen
import com.geeksville.mesh.ui.intro.AppIntroductionScreen
import dagger.hilt.android.AndroidEntryPoint
import org.meshtastic.core.datastore.UiPreferencesDataSource
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
import org.meshtastic.core.ui.theme.AppTheme
import org.meshtastic.core.ui.theme.MODE_DYNAMIC
import org.meshtastic.feature.intro.AppIntroductionScreen
import timber.log.Timber
import javax.inject.Inject

View file

@ -1,132 +0,0 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.intro
import android.Manifest
import android.content.Intent
import android.os.Build
import android.provider.Settings
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.navigation3.runtime.NavKey
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.accompanist.permissions.rememberPermissionState
import kotlinx.serialization.Serializable
/**
* Composable function for the main application introduction screen. This screen guides the user through initial setup
* steps like granting permissions.
*
* @param onDone Callback invoked when the introduction flow is completed.
*/
@OptIn(ExperimentalPermissionsApi::class)
@Suppress("LongMethod")
@Composable
fun AppIntroductionScreen(onDone: () -> Unit) {
val context = LocalContext.current
val notificationPermissionState: PermissionState? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
} else {
null
}
val locationPermissions =
listOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)
val locationPermissionState = rememberMultiplePermissionsState(permissions = locationPermissions)
val backStack = rememberNavBackStack(Welcome)
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryProvider =
entryProvider {
entry<Welcome> { WelcomeScreen(onGetStarted = { backStack.add(Notifications) }) }
entry<Notifications> {
val notificationsAlreadyGranted = notificationPermissionState?.status?.isGranted ?: true
NotificationsScreen(
showNextButton = notificationsAlreadyGranted,
onSkip = {
// Skip this screen and the Critical Alerts screen. Proceed to Location screen.
backStack.add(Location)
},
onConfigure = {
if (notificationsAlreadyGranted) {
backStack.add(CriticalAlerts)
} else {
// For Android Tiramisu (API 33) and above, this requests POST_NOTIFICATIONS
// For lower versions, notificationPermissionState will be null, and this branch isn't
// taken.
notificationPermissionState?.launchPermissionRequest()
}
},
)
}
entry<CriticalAlerts> {
CriticalAlertsScreen(
onSkip = { backStack.add(Location) },
onConfigure = {
// Intent to open the specific notification channel settings for "my_alerts"
// This allows the user to enable critical alerts if they were initially denied
// or to adjust settings for notifications that can bypass Do Not Disturb.
val intent =
Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
putExtra(Settings.EXTRA_CHANNEL_ID, "my_alerts")
}
context.startActivity(intent)
backStack.add(Location)
},
)
}
entry<Location> {
val locationAlreadyGranted = locationPermissionState.allPermissionsGranted
LocationScreen(
showNextButton = locationAlreadyGranted,
onSkip = onDone, // Callback to signify completion of the intro flow
onConfigure = {
if (locationAlreadyGranted) {
onDone() // Permissions already granted, proceed to finish
} else {
locationPermissionState.launchMultiplePermissionRequest()
}
},
)
}
},
)
}
@Serializable private data object Welcome : NavKey
@Serializable private data object Notifications : NavKey
@Serializable private data object CriticalAlerts : NavKey
@Serializable private data object Location : NavKey

View file

@ -1,76 +0,0 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.intro
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.meshtastic.core.strings.R
/**
* Screen for explaining and guiding the user to configure critical alert settings. This screen is part of the app
* introduction flow and appears after the general notification permission screen.
*
* @param onSkip Callback invoked if the user chooses to skip configuring critical alerts.
* @param onConfigure Callback invoked when the user proceeds to configure critical alerts.
*/
@Composable
internal fun CriticalAlertsScreen(onSkip: () -> Unit, onConfigure: () -> Unit) {
Scaffold(
bottomBar = {
IntroBottomBar(
onSkip = onSkip,
onConfigure = onConfigure,
configureButtonText = stringResource(id = R.string.configure_critical_alerts),
skipButtonText = stringResource(id = R.string.skip),
)
},
) { innerPadding ->
Column(
modifier =
Modifier.fillMaxSize().padding(innerPadding).padding(16.dp).verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResource(R.string.critical_alerts),
style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.Bold),
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(R.string.critical_alerts_dnd_request_text),
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
)
}
}
}

View file

@ -1,34 +0,0 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.intro
import androidx.annotation.StringRes
import androidx.compose.ui.graphics.vector.ImageVector
/**
* Data class representing the UI elements for a feature row in the app introduction.
*
* @param icon The vector asset for the feature icon.
* @param titleRes Optional string resource ID for the feature title.
* @param subtitleRes String resource ID for the feature subtitle.
*/
internal data class FeatureUIData(
val icon: ImageVector,
@StringRes val titleRes: Int? = null,
@StringRes val subtitleRes: Int,
)

View file

@ -1,57 +0,0 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.intro
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
/**
* A common bottom bar used across app introduction screens. Provides consistent "Skip" and "Configure" (or "Next")
* buttons.
*
* @param onSkip Callback for the skip action.
* @param onConfigure Callback for the main configure/next action.
* @param skipButtonText Text for the skip button.
* @param configureButtonText Text for the configure/next button.
* @param showSkipButton Whether to display the skip button. Defaults to true.
*/
@Composable
internal fun IntroBottomBar(
onSkip: () -> Unit,
onConfigure: () -> Unit,
skipButtonText: String,
configureButtonText: String,
showSkipButton: Boolean = true,
) {
BottomAppBar(contentPadding = PaddingValues(horizontal = 16.dp, vertical = 20.dp)) {
if (showSkipButton) {
Button(onClick = onSkip) { Text(skipButtonText) }
}
Spacer(modifier = Modifier.fillMaxWidth().weight(1f))
Button(onClick = onConfigure) { Text(configureButtonText) }
}
}

View file

@ -1,104 +0,0 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.intro
import android.content.Context
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
/** Tag used for identifying clickable annotations in text, specifically for linking to settings. */
internal const val SETTINGS_TAG = "settings_link_tag"
/**
* Displays a row for a feature, including an icon, an optional title, and a subtitle.
*
* @param feature The [FeatureUIData] containing information for the row.
*/
@Composable
internal fun FeatureRow(feature: FeatureUIData) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
Icon(
imageVector = feature.icon,
contentDescription =
feature.titleRes?.let { stringResource(id = it) } ?: stringResource(id = feature.subtitleRes),
modifier = Modifier.padding(end = 16.dp),
tint = MaterialTheme.colorScheme.primary,
)
Column {
feature.titleRes?.let { titleRes ->
Text(
text = stringResource(id = titleRes),
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
)
}
Text(
text = stringResource(id = feature.subtitleRes),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
/**
* Creates an [AnnotatedString] with a clickable portion.
*
* @param fullTextRes String resource for the entire text.
* @param linkTextRes String resource for the portion of text that should be clickable.
* @param tag A tag to identify the annotation.
* @return An [AnnotatedString] with the specified portion styled and annotated.
*/
@Composable
internal fun Context.createClickableAnnotatedString(
@StringRes fullTextRes: Int,
@StringRes linkTextRes: Int,
tag: String,
): AnnotatedString {
val fullText = stringResource(id = fullTextRes)
val linkText = stringResource(id = linkTextRes)
val startIndex = fullText.indexOf(linkText)
return buildAnnotatedString {
append(fullText)
if (startIndex != -1) {
val endIndex = startIndex + linkText.length
addStyle(
style = SpanStyle(color = MaterialTheme.colorScheme.primary, textDecoration = TextDecoration.Underline),
start = startIndex,
end = endIndex,
)
addStringAnnotation(tag = tag, annotation = linkText, start = startIndex, end = endIndex)
}
}
}

View file

@ -1,88 +0,0 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.intro
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.LocationOn
import androidx.compose.material.icons.outlined.Router
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import org.meshtastic.core.strings.R
/**
* Screen for configuring location permissions during the app introduction. It explains why location permissions are
* needed and provides options to grant them or skip.
*
* @param showNextButton Indicates whether to show a "Next" button (if permissions are already granted) or a "Configure"
* button.
* @param onSkip Callback invoked if the user chooses to skip location permission setup.
* @param onConfigure Callback invoked when the user proceeds to configure or grant permissions.
*/
@Composable
internal fun LocationScreen(showNextButton: Boolean, onSkip: () -> Unit, onConfigure: () -> Unit) {
val context = LocalContext.current
val annotatedString =
context.createClickableAnnotatedString(
fullTextRes = R.string.phone_location_description,
linkTextRes = R.string.settings,
tag = SETTINGS_TAG,
)
val features = remember {
listOf(
FeatureUIData(
icon = Icons.Outlined.LocationOn,
titleRes = R.string.share_location,
subtitleRes = R.string.share_location_description,
),
FeatureUIData(
icon = Icons.Outlined.Router,
titleRes = R.string.distance_measurements,
subtitleRes = R.string.distance_measurements_description,
),
FeatureUIData(
icon = Icons.Outlined.Router, // Consider a different icon if appropriate
titleRes = R.string.distance_filters,
subtitleRes = R.string.distance_filters_description,
),
FeatureUIData(
icon = Icons.Outlined.LocationOn, // Consider a different icon if appropriate
titleRes = R.string.mesh_map_location,
subtitleRes = R.string.mesh_map_location_description,
),
)
}
PermissionScreenLayout(
headlineRes = R.string.phone_location,
annotatedDescription = annotatedString,
features = features,
onSkip = onSkip,
onConfigure = onConfigure,
configureButtonTextRes = if (showNextButton) R.string.next else R.string.configure_location_permissions,
onAnnotationClick = {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.fromParts("package", context.packageName, null)
context.startActivity(intent)
},
)
}

View file

@ -1,106 +0,0 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.intro
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.outlined.BatteryAlert
import androidx.compose.material.icons.outlined.Message
import androidx.compose.material.icons.outlined.SpeakerPhone
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.meshtastic.core.strings.R
/**
* Screen for configuring notification permissions during the app introduction. It explains why notification permissions
* are needed and provides options to grant them or skip.
*
* @param showNextButton Indicates whether to show a "Next" button (if permissions are already granted) or a "Configure"
* button.
* @param onSkip Callback invoked if the user chooses to skip notification permission setup.
* @param onConfigure Callback invoked when the user proceeds to configure or grant permissions.
*/
@Composable
internal fun NotificationsScreen(showNextButton: Boolean, onSkip: () -> Unit, onConfigure: () -> Unit) {
val context = LocalContext.current
val annotatedString =
context.createClickableAnnotatedString(
fullTextRes = R.string.notification_permissions_description,
linkTextRes = R.string.settings,
tag = SETTINGS_TAG,
)
val features = remember {
listOf(
FeatureUIData(
icon = Icons.Outlined.Message,
titleRes = R.string.incoming_messages,
subtitleRes = R.string.notifications_for_channel_and_direct_messages,
),
FeatureUIData(
icon = Icons.Outlined.SpeakerPhone,
titleRes = R.string.new_nodes,
subtitleRes = R.string.notifications_for_newly_discovered_nodes,
),
FeatureUIData(
icon = Icons.Outlined.BatteryAlert,
titleRes = R.string.low_battery,
subtitleRes = R.string.notifications_for_low_battery_alerts,
),
)
}
PermissionScreenLayout(
headlineRes = R.string.app_notifications,
annotatedDescription = annotatedString,
features = features,
additionalContent = {
Text(
text = stringResource(R.string.critical_alerts),
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold),
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(16.dp))
FeatureRow(
feature =
FeatureUIData(icon = Icons.Filled.Notifications, subtitleRes = R.string.critical_alerts_description),
)
},
onSkip = onSkip,
onConfigure = onConfigure,
configureButtonTextRes = if (showNextButton) R.string.next else R.string.configure_notification_permissions,
onAnnotationClick = {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.fromParts("package", context.packageName, null)
context.startActivity(intent)
},
)
}

View file

@ -1,129 +0,0 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.intro
import androidx.annotation.StringRes
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
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.input.pointer.pointerInput
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.meshtastic.core.strings.R
/**
* A generic layout for screens within the app introduction flow. It typically presents a headline, a descriptive text
* (potentially with clickable annotations), a list of features, and standard navigation buttons.
*
* @param headlineRes String resource for the main headline of the screen.
* @param annotatedDescription The [AnnotatedString] for the main descriptive text.
* @param features A list of [FeatureUIData] to be displayed using [FeatureRow].
* @param additionalContent Optional composable lambda for adding custom content below the features.
* @param onSkip Callback for the skip action.
* @param onConfigure Callback for the main configure/next action.
* @param configureButtonTextRes String resource for the main action button.
* @param onAnnotationClick Callback invoked when a tagged annotation within [annotatedDescription] is clicked.
*/
@Composable
internal fun PermissionScreenLayout(
@StringRes headlineRes: Int,
annotatedDescription: AnnotatedString,
features: List<FeatureUIData>,
additionalContent: (@Composable () -> Unit)? = null,
onSkip: () -> Unit,
onConfigure: () -> Unit,
@StringRes configureButtonTextRes: Int,
onAnnotationClick: (String) -> Unit,
) {
var textLayoutResult by remember { mutableStateOf<TextLayoutResult?>(null) }
val pressIndicator =
Modifier.pointerInput(Unit) {
detectTapGestures { offset ->
textLayoutResult?.let { layoutResult ->
val position = layoutResult.getOffsetForPosition(offset)
annotatedDescription.getStringAnnotations(
SETTINGS_TAG,
position,
position,
).firstOrNull()?.let { annotation ->
onAnnotationClick(annotation.item)
}
}
}
}
Scaffold(
bottomBar = {
IntroBottomBar(
onSkip = onSkip,
onConfigure = onConfigure,
configureButtonText = stringResource(id = configureButtonTextRes),
skipButtonText = stringResource(id = R.string.skip),
)
},
) { innerPadding ->
Column(
modifier =
Modifier.fillMaxSize().padding(innerPadding).padding(16.dp).verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResource(headlineRes),
style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.Bold),
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = annotatedDescription,
style =
MaterialTheme.typography.bodyLarge.copy(
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
),
modifier = Modifier.padding(horizontal = 16.dp).then(pressIndicator),
onTextLayout = { textLayoutResult = it },
)
Spacer(modifier = Modifier.height(16.dp))
features.forEach { feature ->
FeatureRow(feature = feature)
Spacer(modifier = Modifier.height(16.dp))
}
additionalContent?.invoke()
}
}
}

View file

@ -1,104 +0,0 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.intro
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Hub
import androidx.compose.material.icons.outlined.NearMe
import androidx.compose.material.icons.outlined.SettingsInputAntenna
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.meshtastic.core.strings.R
/**
* The initial welcome screen for the app introduction flow. It displays a brief overview of the app's key features.
*
* @param onGetStarted Callback invoked when the user proceeds from the welcome screen.
*/
@Composable
internal fun WelcomeScreen(onGetStarted: () -> Unit) {
val features = remember {
listOf(
FeatureUIData(
icon = Icons.Outlined.SettingsInputAntenna,
titleRes = R.string.stay_connected_anywhere,
subtitleRes = R.string.communicate_off_the_grid,
),
FeatureUIData(
icon = Icons.Outlined.Hub,
titleRes = R.string.create_your_own_networks,
subtitleRes = R.string.easily_set_up_private_mesh_networks,
),
FeatureUIData(
icon = Icons.Outlined.NearMe,
titleRes = R.string.track_and_share_locations,
subtitleRes = R.string.share_your_location_in_real_time,
),
)
}
Scaffold(
bottomBar = {
IntroBottomBar(
onSkip = {}, // No skip on welcome
onConfigure = onGetStarted,
skipButtonText = "", // Not shown
configureButtonText = stringResource(id = R.string.get_started),
showSkipButton = false, // Explicitly hide skip for welcome
)
},
) { innerPadding ->
Column(
modifier =
Modifier.fillMaxSize().padding(innerPadding).padding(16.dp).verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResource(R.string.intro_welcome),
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold),
textAlign = TextAlign.Center,
)
Text(
text = stringResource(R.string.meshtastic),
style = MaterialTheme.typography.headlineLarge.copy(fontWeight = FontWeight.Bold),
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(32.dp))
features.forEach { feature ->
FeatureRow(feature = feature)
Spacer(modifier = Modifier.height(16.dp))
}
}
}
}