mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
ask existing users with the setup guide's usage page instead of a dialog
This commit is contained in:
1 parent
b5ff4ec22b
commit
7cc6b64ce2
3 files changed
+43
-70
No files matched your search
@@ -37,8 +37,9 @@ import androidx.compose.ui.unit.dp
|
||||
import app.seb3thehacker.gearslip.AppSettings
|
||||
import app.seb3thehacker.gearslip.BuildConfig
|
||||
import app.seb3thehacker.gearslip.CertProvider
|
||||
import app.seb3thehacker.gearslip.stats.UsageStats
|
||||
|
||||
private enum class Screen { HOME, LOGS, SETTINGS, CAR_PREVIEW, CERT_SETUP, SETUP_GUIDE, HELP, WHATS_NEW, RECOMMENDED_APPS }
|
||||
private enum class Screen { HOME, LOGS, SETTINGS, CAR_PREVIEW, CERT_SETUP, SETUP_GUIDE, USAGE_NOTES, HELP, WHATS_NEW, RECOMMENDED_APPS }
|
||||
|
||||
/** Three screens and a back stack of depth one: no navigation library needed. */
|
||||
@Composable
|
||||
@@ -53,6 +54,8 @@ fun GearslipApp(onDisconnect: () -> Unit, onRequestCallScreening: () -> Unit) {
|
||||
when {
|
||||
needsCertSetup -> Screen.CERT_SETUP
|
||||
needsSetupGuide -> Screen.SETUP_GUIDE
|
||||
// Set up before usage notes existed: ask once, on the setup guide's own page.
|
||||
!UsageStats.asked(context) -> Screen.USAGE_NOTES
|
||||
else -> Screen.HOME
|
||||
},
|
||||
)
|
||||
@@ -120,6 +123,11 @@ fun GearslipApp(onDisconnect: () -> Unit, onRequestCallScreening: () -> Unit) {
|
||||
onRequestCallScreening = onRequestCallScreening,
|
||||
onDone = { screen = Screen.HOME },
|
||||
)
|
||||
Screen.USAGE_NOTES -> SetupGuideScreen(
|
||||
onRequestCallScreening = onRequestCallScreening,
|
||||
onDone = { screen = Screen.HOME },
|
||||
onlyUsageNotes = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
// A car keyboard is open: offer the phone's own, for a passenger.
|
||||
@@ -127,8 +135,6 @@ fun GearslipApp(onDisconnect: () -> Unit, onRequestCallScreening: () -> Unit) {
|
||||
}
|
||||
|
||||
CompatWarning()
|
||||
// Anyone who set up before usage notes existed gets asked once, on Home.
|
||||
if (screen == Screen.HOME) UsageStatsPrompt()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -100,6 +100,21 @@ private fun hasNotificationAccess(context: Context): Boolean {
|
||||
return runCatching { manager.isNotificationListenerAccessGranted(component) }.getOrDefault(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* The usage notes question: the setup guide's last page, and on its own the one screen people
|
||||
* who set up before the notes existed see once. Leaving it without turning them on is a no.
|
||||
*/
|
||||
private fun usageNotesPage(context: Context) = GuidePage(
|
||||
title = USAGE_STATS_TITLE,
|
||||
explanation = USAGE_STATS_INTRO,
|
||||
content = {
|
||||
UsageStatsChoices()
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { if (!UsageStats.asked(context)) UsageStats.set(context, false) }
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
private fun pages(context: Context, onRequestCallScreening: () -> Unit): List<GuidePage> = buildList {
|
||||
// --- Safety: always first, and never skipped by an already-granted check ---------
|
||||
add(
|
||||
@@ -285,20 +300,9 @@ private fun pages(context: Context, onRequestCallScreening: () -> Unit): List<Gu
|
||||
)
|
||||
}
|
||||
|
||||
// --- The very end, every time: usage notes. The first time through, leaving the page
|
||||
// without turning them on is a no; on a replay, the switch shows what was chosen before.
|
||||
add(
|
||||
GuidePage(
|
||||
title = USAGE_STATS_TITLE,
|
||||
explanation = USAGE_STATS_INTRO,
|
||||
content = {
|
||||
UsageStatsChoices()
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { if (!UsageStats.asked(context)) UsageStats.set(context, false) }
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
// --- The very end, every time: usage notes. On a replay, the switch shows what was
|
||||
// chosen before.
|
||||
add(usageNotesPage(context))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -308,9 +312,9 @@ private fun pages(context: Context, onRequestCallScreening: () -> Unit): List<Gu
|
||||
* driver takes on trust, with "Skip" sitting right next to it.
|
||||
*/
|
||||
@Composable
|
||||
fun SetupGuideScreen(onRequestCallScreening: () -> Unit, onDone: () -> Unit) {
|
||||
fun SetupGuideScreen(onRequestCallScreening: () -> Unit, onDone: () -> Unit, onlyUsageNotes: Boolean = false) {
|
||||
val context = LocalContext.current
|
||||
val pages = remember { pages(context, onRequestCallScreening) }
|
||||
val pages = remember { if (onlyUsageNotes) listOf(usageNotesPage(context)) else pages(context, onRequestCallScreening) }
|
||||
var index by remember { mutableIntStateOf(0) }
|
||||
|
||||
// Only ours to handle once there is a previous page to return to; at the first page, the
|
||||
@@ -347,17 +351,20 @@ fun SetupGuideScreen(onRequestCallScreening: () -> Unit, onDone: () -> Unit) {
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Spacer(Modifier.height(24.dp))
|
||||
LinearProgressIndicator(
|
||||
progress = { (index + 1f) / pages.size },
|
||||
modifier = Modifier.fillMaxWidth().height(10.dp).clip(MaterialTheme.shapes.extraLarge),
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text(
|
||||
"${index + 1} of ${pages.size}",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
// A single page has no progress to show.
|
||||
if (pages.size > 1) {
|
||||
LinearProgressIndicator(
|
||||
progress = { (index + 1f) / pages.size },
|
||||
modifier = Modifier.fillMaxWidth().height(10.dp).clip(MaterialTheme.shapes.extraLarge),
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text(
|
||||
"${index + 1} of ${pages.size}",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
// The page scrolls between the progress bar and the buttons, so a tall page (the usage
|
||||
// note's choices, say) never pushes Next off the screen. Short pages stay centred.
|
||||
BoxWithConstraints(Modifier.weight(1f).fillMaxWidth()) {
|
||||
|
||||
@@ -2,28 +2,23 @@ package app.seb3thehacker.gearslip.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||
import androidx.compose.material.icons.filled.Send
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -158,38 +153,3 @@ private fun Choice(label: String, checked: Boolean, onChange: (Boolean) -> Unit)
|
||||
Checkbox(checked = checked, onCheckedChange = null, modifier = Modifier.padding(vertical = 8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks once, for people who set Gearslip up before usage notes existed and so never saw the
|
||||
* setup page. Either answer is final; Settings changes it later.
|
||||
*/
|
||||
@Composable
|
||||
internal fun UsageStatsPrompt() {
|
||||
val context = LocalContext.current
|
||||
var show by remember { mutableStateOf(!UsageStats.asked(context)) }
|
||||
if (!show) return
|
||||
AlertDialog(
|
||||
// An answer is needed: tapping outside would just bring it back next time.
|
||||
onDismissRequest = {},
|
||||
title = { Text(USAGE_STATS_TITLE) },
|
||||
text = {
|
||||
Column(Modifier.verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text(USAGE_STATS_INTRO)
|
||||
Text(
|
||||
"Turning this on also shares the Android version, the phone model, and the cars you " +
|
||||
"connect to. You can untick any of them in Settings.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
UsageStats.set(context, true)
|
||||
show = false
|
||||
UsageStats.choicesClosed(context)
|
||||
}) { Text("Turn on") }
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { UsageStats.set(context, false); show = false }) { Text("No thanks") } },
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user