mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-11 00:12:24 +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.AppSettings
|
||||||
import app.seb3thehacker.gearslip.BuildConfig
|
import app.seb3thehacker.gearslip.BuildConfig
|
||||||
import app.seb3thehacker.gearslip.CertProvider
|
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. */
|
/** Three screens and a back stack of depth one: no navigation library needed. */
|
||||||
@Composable
|
@Composable
|
||||||
@@ -53,6 +54,8 @@ fun GearslipApp(onDisconnect: () -> Unit, onRequestCallScreening: () -> Unit) {
|
|||||||
when {
|
when {
|
||||||
needsCertSetup -> Screen.CERT_SETUP
|
needsCertSetup -> Screen.CERT_SETUP
|
||||||
needsSetupGuide -> Screen.SETUP_GUIDE
|
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
|
else -> Screen.HOME
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -120,6 +123,11 @@ fun GearslipApp(onDisconnect: () -> Unit, onRequestCallScreening: () -> Unit) {
|
|||||||
onRequestCallScreening = onRequestCallScreening,
|
onRequestCallScreening = onRequestCallScreening,
|
||||||
onDone = { screen = Screen.HOME },
|
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.
|
// A car keyboard is open: offer the phone's own, for a passenger.
|
||||||
@@ -127,8 +135,6 @@ fun GearslipApp(onDisconnect: () -> Unit, onRequestCallScreening: () -> Unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CompatWarning()
|
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)
|
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 {
|
private fun pages(context: Context, onRequestCallScreening: () -> Unit): List<GuidePage> = buildList {
|
||||||
// --- Safety: always first, and never skipped by an already-granted check ---------
|
// --- Safety: always first, and never skipped by an already-granted check ---------
|
||||||
add(
|
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
|
// --- The very end, every time: usage notes. On a replay, the switch shows what was
|
||||||
// without turning them on is a no; on a replay, the switch shows what was chosen before.
|
// chosen before.
|
||||||
add(
|
add(usageNotesPage(context))
|
||||||
GuidePage(
|
|
||||||
title = USAGE_STATS_TITLE,
|
|
||||||
explanation = USAGE_STATS_INTRO,
|
|
||||||
content = {
|
|
||||||
UsageStatsChoices()
|
|
||||||
DisposableEffect(Unit) {
|
|
||||||
onDispose { if (!UsageStats.asked(context)) UsageStats.set(context, false) }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -308,9 +312,9 @@ private fun pages(context: Context, onRequestCallScreening: () -> Unit): List<Gu
|
|||||||
* driver takes on trust, with "Skip" sitting right next to it.
|
* driver takes on trust, with "Skip" sitting right next to it.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun SetupGuideScreen(onRequestCallScreening: () -> Unit, onDone: () -> Unit) {
|
fun SetupGuideScreen(onRequestCallScreening: () -> Unit, onDone: () -> Unit, onlyUsageNotes: Boolean = false) {
|
||||||
val context = LocalContext.current
|
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) }
|
var index by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
// Only ours to handle once there is a previous page to return to; at the first page, the
|
// 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,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
LinearProgressIndicator(
|
// A single page has no progress to show.
|
||||||
progress = { (index + 1f) / pages.size },
|
if (pages.size > 1) {
|
||||||
modifier = Modifier.fillMaxWidth().height(10.dp).clip(MaterialTheme.shapes.extraLarge),
|
LinearProgressIndicator(
|
||||||
)
|
progress = { (index + 1f) / pages.size },
|
||||||
Spacer(Modifier.height(12.dp))
|
modifier = Modifier.fillMaxWidth().height(10.dp).clip(MaterialTheme.shapes.extraLarge),
|
||||||
Text(
|
)
|
||||||
"${index + 1} of ${pages.size}",
|
Spacer(Modifier.height(12.dp))
|
||||||
style = MaterialTheme.typography.titleMedium,
|
Text(
|
||||||
fontWeight = FontWeight.SemiBold,
|
"${index + 1} of ${pages.size}",
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
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
|
// 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.
|
// note's choices, say) never pushes Next off the screen. Short pages stay centred.
|
||||||
BoxWithConstraints(Modifier.weight(1f).fillMaxWidth()) {
|
BoxWithConstraints(Modifier.weight(1f).fillMaxWidth()) {
|
||||||
|
|||||||
@@ -2,28 +2,23 @@ package app.seb3thehacker.gearslip.ui
|
|||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.selection.toggleable
|
import androidx.compose.foundation.selection.toggleable
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Info
|
import androidx.compose.material.icons.filled.Info
|
||||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||||
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||||
import androidx.compose.material.icons.filled.Send
|
import androidx.compose.material.icons.filled.Send
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.getValue
|
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))
|
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