mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
redesign the phone app's settings
One look for every section, What's new and the setup guide at the top, and the setup guide's buttons stay on screen. Recommended apps is hidden for now.
This commit is contained in:
1 parent
5a3bbe82a0
commit
cf0c324d80
6 files changed
+325
-133
No files matched your search
@@ -86,7 +86,7 @@ private fun setupSteps(onRequestCallScreening: () -> Unit) = listOf(
|
||||
* here too so it can be reread without repeating the guide itself. */
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun HelpScreen(onBack: () -> Unit, onRequestCallScreening: () -> Unit, onReplayTutorial: () -> Unit) {
|
||||
fun HelpScreen(onBack: () -> Unit, onRequestCallScreening: () -> Unit) {
|
||||
val steps = remember(onRequestCallScreening) { setupSteps(onRequestCallScreening) }
|
||||
|
||||
Scaffold(
|
||||
@@ -110,10 +110,7 @@ fun HelpScreen(onBack: () -> Unit, onRequestCallScreening: () -> Unit, onReplayT
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
FilledTonalButton(onClick = onReplayTutorial, modifier = Modifier.fillMaxWidth().height(52.dp)) {
|
||||
Text("Replay the guided tutorial")
|
||||
}
|
||||
Spacer(Modifier.height(24.dp))
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Column(verticalArrangement = Arrangement.spacedBy(18.dp)) {
|
||||
steps.forEachIndexed { index, step ->
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
|
||||
@@ -17,7 +17,6 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.Send
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material.icons.filled.List
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
@@ -49,7 +48,6 @@ fun HomeScreen(
|
||||
onOpenSettings: () -> Unit,
|
||||
onOpenCarPreview: () -> Unit,
|
||||
onOpenHelp: () -> Unit,
|
||||
onOpenWhatsNew: () -> Unit,
|
||||
onDisconnect: () -> Unit,
|
||||
) {
|
||||
val status by SessionStatus.state.collectAsStateWithLifecycle()
|
||||
@@ -115,8 +113,6 @@ fun HomeScreen(
|
||||
ActionRow(Icons.Filled.List, "Live logs", onOpenLogs)
|
||||
RowDivider()
|
||||
ActionRow(Icons.Filled.Info, "Connection help", onOpenHelp)
|
||||
RowDivider()
|
||||
ActionRow(Icons.Filled.Star, "What's new", onOpenWhatsNew)
|
||||
if (BuildConfig.DEBUG) {
|
||||
val context = LocalContext.current
|
||||
RowDivider()
|
||||
|
||||
@@ -92,6 +92,9 @@ fun RecommendedAppsScreen(onBack: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Hidden for now: off, neither Settings nor the setup guide shows the list. The code stays. */
|
||||
internal const val SHOW_RECOMMENDED_APPS = false
|
||||
|
||||
internal const val RECOMMENDED_INTRO =
|
||||
"These free apps run on Gearslip's car screen. Each button opens the app's page on GitHub."
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package app.seb3thehacker.gearslip.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/*
|
||||
* The pieces every phone settings screen is built from, so they all share one shape, colour and
|
||||
* spacing: a section label, a card holding rows, and the rows themselves, with a footer note
|
||||
* under the card when a section needs one.
|
||||
*/
|
||||
|
||||
/** A titled group: label above, rows in one card, an optional note below. */
|
||||
@Composable
|
||||
internal fun SettingsSection(
|
||||
title: String? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
header: AnnotatedString? = null,
|
||||
footer: String? = null,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
Column(modifier.fillMaxWidth()) {
|
||||
title?.let {
|
||||
Text(
|
||||
it,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(start = 16.dp, bottom = 8.dp),
|
||||
)
|
||||
}
|
||||
header?.let {
|
||||
Text(
|
||||
it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 12.dp),
|
||||
)
|
||||
}
|
||||
SettingsCard(content = content)
|
||||
footer?.let {
|
||||
Text(
|
||||
it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The card behind a group of rows; also used on its own, as in the setup guide. */
|
||||
@Composable
|
||||
internal fun SettingsCard(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
Column(content = content)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One row: an optional icon, a title with an optional line under it, and whatever sits at the
|
||||
* end (a switch, a checkbox, an arrow). [modifier] carries the click or toggle, so the whole
|
||||
* row is the touch target.
|
||||
*/
|
||||
@Composable
|
||||
internal fun SettingsRow(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
subtitle: String? = null,
|
||||
icon: ImageVector? = null,
|
||||
tint: Color = MaterialTheme.colorScheme.primary,
|
||||
titleColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||
trailing: (@Composable () -> Unit)? = null,
|
||||
) {
|
||||
Row(
|
||||
modifier.fillMaxWidth().heightIn(min = 56.dp).padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
if (icon != null) Icon(icon, contentDescription = null, tint = tint, modifier = Modifier.size(24.dp))
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(title, style = MaterialTheme.typography.bodyLarge, color = titleColor)
|
||||
subtitle?.let {
|
||||
Text(it, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
trailing?.let { Box { it() } }
|
||||
}
|
||||
}
|
||||
|
||||
/** A thin line between rows in one card. */
|
||||
@Composable
|
||||
internal fun SettingsDivider() {
|
||||
HorizontalDivider(Modifier.padding(horizontal = 16.dp), color = MaterialTheme.colorScheme.outlineVariant)
|
||||
}
|
||||
@@ -3,30 +3,37 @@ package app.seb3thehacker.gearslip.ui
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.selection.toggleable
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowDown
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
@@ -39,22 +46,28 @@ import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
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
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(onBack: () -> Unit, onOpenRecommendedApps: () -> Unit) {
|
||||
fun SettingsScreen(
|
||||
onBack: () -> Unit,
|
||||
onOpenRecommendedApps: () -> Unit,
|
||||
onOpenWhatsNew: () -> Unit,
|
||||
onReplayTutorial: () -> Unit,
|
||||
) {
|
||||
Scaffold(
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
topBar = {
|
||||
@@ -74,59 +87,78 @@ fun SettingsScreen(onBack: () -> Unit, onOpenRecommendedApps: () -> Unit) {
|
||||
.padding(padding)
|
||||
.padding(horizontal = 24.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(32.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
Spacer(Modifier.height(0.dp))
|
||||
GearslipEnabledSection()
|
||||
AppsSection(onOpenRecommendedApps)
|
||||
AboutSection(onOpenWhatsNew, onReplayTutorial)
|
||||
ConnectionSection()
|
||||
if (SHOW_RECOMMENDED_APPS) AppsSection(onOpenRecommendedApps)
|
||||
CertificateSection()
|
||||
if (UsageStats.choosable) {
|
||||
SettingsSection("Usage notes", header = USAGE_STATS_INTRO) { UsageStatsRows(showNote = true) }
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SectionTitle(text: String) {
|
||||
Text(
|
||||
text,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
private fun Arrow() = Icon(
|
||||
Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
/** What's new and the setup guide, one tap from the top. */
|
||||
@Composable
|
||||
private fun AboutSection(onOpenWhatsNew: () -> Unit, onReplayTutorial: () -> Unit) {
|
||||
SettingsSection {
|
||||
SettingsRow(
|
||||
"What's new",
|
||||
Modifier.clickable(onClick = onOpenWhatsNew),
|
||||
subtitle = "Version ${BuildConfig.VERSION_NAME}",
|
||||
icon = Icons.Filled.Star,
|
||||
trailing = { Arrow() },
|
||||
)
|
||||
SettingsDivider()
|
||||
SettingsRow(
|
||||
"Replay the setup guide",
|
||||
Modifier.clickable(onClick = onReplayTutorial),
|
||||
subtitle = "Go through the first-run steps again",
|
||||
icon = Icons.Filled.Refresh,
|
||||
trailing = { Arrow() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Off, a stock Android Auto app or head unit answers the USB connection instead of Gearslip. */
|
||||
@Composable
|
||||
private fun GearslipEnabledSection() {
|
||||
private fun ConnectionSection() {
|
||||
val context = LocalContext.current
|
||||
var enabled by remember { mutableStateOf(AppSettings.gearslipEnabled(context)) }
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
SectionTitle("Gearslip")
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Text("Enabled", style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f))
|
||||
Switch(checked = enabled, onCheckedChange = {
|
||||
SettingsSection("Connection") {
|
||||
SettingsRow(
|
||||
"Use Gearslip in the car",
|
||||
Modifier.toggleable(enabled, role = Role.Switch) {
|
||||
enabled = it
|
||||
AppSettings.setGearslipEnabled(context, it)
|
||||
})
|
||||
}
|
||||
if (!enabled) {
|
||||
Text(
|
||||
"Off - stock Android Auto can take the USB connection instead.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
},
|
||||
subtitle = if (enabled) "Gearslip answers when you plug into a car" else "Off: Android Auto can take the connection instead",
|
||||
icon = Icons.Filled.Settings,
|
||||
trailing = { Switch(checked = enabled, onCheckedChange = null) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppsSection(onOpenRecommendedApps: () -> Unit) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
SectionTitle("Apps")
|
||||
OutlinedButton(onClick = onOpenRecommendedApps, modifier = Modifier.fillMaxWidth().height(52.dp)) {
|
||||
Text("Recommended apps")
|
||||
}
|
||||
SettingsSection("Apps") {
|
||||
SettingsRow(
|
||||
"Recommended apps",
|
||||
Modifier.clickable(onClick = onOpenRecommendedApps),
|
||||
icon = Icons.Filled.Star,
|
||||
trailing = { Arrow() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,37 +174,49 @@ private fun CertificateSection() {
|
||||
|
||||
var pendingUri by remember { mutableStateOf<Uri?>(null) }
|
||||
var notice by remember { mutableStateOf<String?>(null) }
|
||||
var downloading by remember { mutableStateOf(false) }
|
||||
var detailsOpen by remember { mutableStateOf(false) }
|
||||
val picker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
|
||||
if (uri != null) pendingUri = uri
|
||||
}
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
SectionTitle("Certificate")
|
||||
|
||||
Surface(color = MaterialTheme.colorScheme.surfaceContainerHigh, shape = MaterialTheme.shapes.large) {
|
||||
Column(Modifier.fillMaxWidth().padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
val s = summary
|
||||
if (s == null) {
|
||||
Text("Checking…", style = MaterialTheme.typography.bodyMedium)
|
||||
} else {
|
||||
Text(s.headline, style = MaterialTheme.typography.bodyLarge)
|
||||
s.subject?.let { Detail("Subject", it) }
|
||||
s.issuer?.let { Detail("Issuer", it) }
|
||||
s.validUntil?.let { Detail("Valid until", it) }
|
||||
SettingsSection("Certificate", footer = notice ?: "Changes take effect on the next connection.") {
|
||||
val s = summary
|
||||
val loaded = s != null && s.kind != CertProvider.Kind.SELF_SIGNED
|
||||
val hasDetails = s?.subject != null || s?.issuer != null
|
||||
SettingsRow(
|
||||
s?.headline ?: "Checking…",
|
||||
if (hasDetails) Modifier.clickable { detailsOpen = !detailsOpen } else Modifier,
|
||||
subtitle = s?.validUntil?.let { "Valid until $it" },
|
||||
icon = if (s == null || loaded) Icons.Filled.Lock else Icons.Filled.Warning,
|
||||
tint = if (s == null || loaded) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
|
||||
trailing = if (hasDetails) {
|
||||
{
|
||||
Icon(
|
||||
if (detailsOpen) Icons.Filled.KeyboardArrowUp else Icons.Filled.KeyboardArrowDown,
|
||||
contentDescription = if (detailsOpen) "Hide details" else "Show details",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
} else null,
|
||||
)
|
||||
AnimatedVisibility(detailsOpen && hasDetails) {
|
||||
Column(
|
||||
Modifier.padding(start = 56.dp, end = 16.dp, bottom = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
s?.subject?.let { Detail("Subject", it) }
|
||||
s?.issuer?.let { Detail("Issuer", it) }
|
||||
}
|
||||
}
|
||||
|
||||
Button(onClick = { picker.launch(arrayOf("*/*")) }, modifier = Modifier.fillMaxWidth().height(52.dp)) {
|
||||
Text("Import certificate (.p12)")
|
||||
}
|
||||
|
||||
FilledTonalButton(
|
||||
onClick = {
|
||||
SettingsDivider()
|
||||
SettingsRow(
|
||||
if (downloading) "Downloading…" else "Download certificate",
|
||||
Modifier.clickable(enabled = !downloading) {
|
||||
downloading = true
|
||||
scope.launch {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
runCatching { CertProvider.downloadAndImport(context) }
|
||||
}
|
||||
val result = withContext(Dispatchers.IO) { runCatching { CertProvider.downloadAndImport(context) } }
|
||||
downloading = false
|
||||
result.onSuccess {
|
||||
notice = "Certificate downloaded."
|
||||
refresh++
|
||||
@@ -181,39 +225,34 @@ private fun CertificateSection() {
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth().height(52.dp),
|
||||
) {
|
||||
Text("Download certificate")
|
||||
}
|
||||
|
||||
if (summary?.kind == CertProvider.Kind.IMPORTED) {
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
CertProvider.removeImported(context)
|
||||
notice = "Removed the imported certificate."
|
||||
refresh++
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth().height(52.dp),
|
||||
) { Text("Remove imported certificate") }
|
||||
}
|
||||
|
||||
if (summary?.kind == CertProvider.Kind.DOWNLOADED) {
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
CertProvider.removeDownloaded(context)
|
||||
notice = "Removed the downloaded certificate."
|
||||
refresh++
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth().height(52.dp),
|
||||
) { Text("Remove downloaded certificate") }
|
||||
}
|
||||
|
||||
notice?.let { Text(it, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) }
|
||||
Text(
|
||||
"Changes take effect on the next connection.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
icon = Icons.Filled.Refresh,
|
||||
)
|
||||
SettingsDivider()
|
||||
SettingsRow(
|
||||
"Import a certificate",
|
||||
Modifier.clickable { picker.launch(arrayOf("*/*")) },
|
||||
subtitle = "A .p12 file with its private key",
|
||||
icon = Icons.Filled.Add,
|
||||
)
|
||||
val removable = when (s?.kind) {
|
||||
CertProvider.Kind.IMPORTED -> "Remove imported certificate"
|
||||
CertProvider.Kind.DOWNLOADED -> "Remove downloaded certificate"
|
||||
else -> null
|
||||
}
|
||||
if (removable != null) {
|
||||
SettingsDivider()
|
||||
SettingsRow(
|
||||
removable,
|
||||
Modifier.clickable {
|
||||
if (s?.kind == CertProvider.Kind.IMPORTED) CertProvider.removeImported(context) else CertProvider.removeDownloaded(context)
|
||||
notice = if (s?.kind == CertProvider.Kind.IMPORTED) "Removed the imported certificate." else "Removed the downloaded certificate."
|
||||
refresh++
|
||||
},
|
||||
icon = Icons.Filled.Delete,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
titleColor = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pendingUri?.let { uri ->
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package app.seb3thehacker.gearslip.ui
|
||||
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.key
|
||||
import app.seb3thehacker.gearslip.BuildConfig
|
||||
import android.Manifest
|
||||
import android.app.AppOpsManager
|
||||
@@ -40,6 +45,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.seb3thehacker.gearslip.AppSettings
|
||||
@@ -47,6 +53,8 @@ import app.seb3thehacker.gearslip.call.CarCalls
|
||||
import app.seb3thehacker.gearslip.car.Prefetch
|
||||
import app.seb3thehacker.gearslip.host.AndroidAuto
|
||||
import app.seb3thehacker.gearslip.notify.GearslipNotificationListener
|
||||
import app.seb3thehacker.gearslip.stats.UsageStats
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
|
||||
/**
|
||||
* One page of the guide. [permissions] drives a system permission dialog; [command] is an adb
|
||||
@@ -56,7 +64,8 @@ import app.seb3thehacker.gearslip.notify.GearslipNotificationListener
|
||||
*/
|
||||
private class GuidePage(
|
||||
val title: String,
|
||||
val explanation: String,
|
||||
/** Plain text, or an [AnnotatedString] when part of it needs to stand out. */
|
||||
val explanation: CharSequence,
|
||||
val permissions: Array<String>? = null,
|
||||
val command: String? = null,
|
||||
val actionLabel: String? = null,
|
||||
@@ -265,14 +274,34 @@ private fun pages(context: Context, onRequestCallScreening: () -> Unit): List<Gu
|
||||
)
|
||||
}
|
||||
|
||||
// --- Last: apps worth installing, once everything above is set up ---------------
|
||||
add(
|
||||
GuidePage(
|
||||
title = "Recommended apps",
|
||||
explanation = RECOMMENDED_INTRO,
|
||||
content = { RecommendedAppsList() },
|
||||
),
|
||||
)
|
||||
// --- Apps worth installing, once everything above is set up -----------------------
|
||||
if (SHOW_RECOMMENDED_APPS) {
|
||||
add(
|
||||
GuidePage(
|
||||
title = "Recommended apps",
|
||||
explanation = RECOMMENDED_INTRO,
|
||||
content = { RecommendedAppsList() },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// --- 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.
|
||||
// Dev builds always send, so they skip the page.
|
||||
if (UsageStats.choosable) {
|
||||
add(
|
||||
GuidePage(
|
||||
title = USAGE_STATS_TITLE,
|
||||
explanation = USAGE_STATS_INTRO,
|
||||
content = {
|
||||
UsageStatsChoices()
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { if (!UsageStats.asked(context)) UsageStats.set(context, false) }
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -319,8 +348,8 @@ fun SetupGuideScreen(onRequestCallScreening: () -> Unit, onDone: () -> Unit) {
|
||||
.padding(padding)
|
||||
.padding(horizontal = 24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Spacer(Modifier.height(24.dp))
|
||||
LinearProgressIndicator(
|
||||
progress = { (index + 1f) / pages.size },
|
||||
modifier = Modifier.fillMaxWidth().height(10.dp).clip(MaterialTheme.shapes.extraLarge),
|
||||
@@ -332,28 +361,39 @@ fun SetupGuideScreen(onRequestCallScreening: () -> Unit, onDone: () -> Unit) {
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(Modifier.height(48.dp))
|
||||
|
||||
Surface(color = MaterialTheme.colorScheme.surfaceContainerLow, shape = MaterialTheme.shapes.extraLarge) {
|
||||
Column(Modifier.fillMaxWidth().padding(24.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text(
|
||||
page.title,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Text(
|
||||
page.explanation,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
page.command?.let { CopyableCommand(it) }
|
||||
page.content?.invoke()
|
||||
// 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()) {
|
||||
key(index) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.heightIn(min = maxHeight)
|
||||
.padding(vertical = 24.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Surface(color = MaterialTheme.colorScheme.surfaceContainerLow, shape = MaterialTheme.shapes.extraLarge) {
|
||||
Column(Modifier.fillMaxWidth().padding(24.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text(
|
||||
page.title,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Text(
|
||||
page.explanation as? AnnotatedString ?: AnnotatedString(page.explanation.toString()),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
page.command?.let { CopyableCommand(it) }
|
||||
page.content?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(32.dp))
|
||||
|
||||
when {
|
||||
page.permissions != null && alreadyGranted -> {
|
||||
Text(
|
||||
@@ -391,6 +431,7 @@ fun SetupGuideScreen(onRequestCallScreening: () -> Unit, onDone: () -> Unit) {
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(24.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user