mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
reccomend apps to install during setup
This commit is contained in:
1 parent
1b44d2d798
commit
96d16d048c
5 files changed
+198
-6
No files matched your search
@@ -16,7 +16,6 @@ object KnownApps {
|
||||
"in.krosbits.musicolet", // Musicolet: browse, play, seek, lyrics
|
||||
"com.kododake.aabrowser", // Aardvark: browser template, starts, draws, takes touches
|
||||
"com.vivi.vivimusic", // Vivi Music: browse, play, seek, works well
|
||||
"com.mapquest.android.ace", // MapQuest: map, search, routing - works well
|
||||
"nl.flitsmeister", // Flitsmeister: map, routing, alerts - works well
|
||||
)
|
||||
|
||||
@@ -68,6 +67,7 @@ object KnownApps {
|
||||
/** Shown on the launcher with a yellow mark: starts and can be used, but not cleanly enough to call working. */
|
||||
private val partial = setOf(
|
||||
"app.vela", // Vela Maps: connects and draws, but rough enough not to call it working yet
|
||||
"com.mapquest.android.ace", // MapQuest: map, search and routing run, but not cleanly
|
||||
)
|
||||
|
||||
fun isPartial(packageName: String): Boolean = packageName in partial
|
||||
|
||||
@@ -38,7 +38,7 @@ import app.seb3thehacker.gearslip.AppSettings
|
||||
import app.seb3thehacker.gearslip.BuildConfig
|
||||
import app.seb3thehacker.gearslip.CertProvider
|
||||
|
||||
private enum class Screen { HOME, LOGS, SETTINGS, CAR_PREVIEW, CERT_SETUP, SETUP_GUIDE, HELP, WHATS_NEW }
|
||||
private enum class Screen { HOME, LOGS, SETTINGS, CAR_PREVIEW, CERT_SETUP, SETUP_GUIDE, HELP, WHATS_NEW, RECOMMENDED_APPS }
|
||||
|
||||
/** Three screens and a back stack of depth one: no navigation library needed. */
|
||||
@Composable
|
||||
@@ -57,7 +57,9 @@ fun GearslipApp(onDisconnect: () -> Unit, onRequestCallScreening: () -> Unit) {
|
||||
},
|
||||
)
|
||||
}
|
||||
BackHandler(enabled = screen != Screen.HOME) { screen = Screen.HOME }
|
||||
BackHandler(enabled = screen != Screen.HOME) {
|
||||
screen = if (screen == Screen.RECOMMENDED_APPS) Screen.SETTINGS else Screen.HOME
|
||||
}
|
||||
|
||||
// Home stays upright; every other screen turns with the phone.
|
||||
LaunchedEffect(screen) {
|
||||
@@ -98,7 +100,11 @@ fun GearslipApp(onDisconnect: () -> Unit, onRequestCallScreening: () -> Unit) {
|
||||
)
|
||||
Screen.WHATS_NEW -> WhatsNewScreen(onBack = { screen = Screen.HOME })
|
||||
Screen.LOGS -> LogsScreen(onBack = { screen = Screen.HOME })
|
||||
Screen.SETTINGS -> SettingsScreen(onBack = { screen = Screen.HOME })
|
||||
Screen.SETTINGS -> SettingsScreen(
|
||||
onBack = { screen = Screen.HOME },
|
||||
onOpenRecommendedApps = { screen = Screen.RECOMMENDED_APPS },
|
||||
)
|
||||
Screen.RECOMMENDED_APPS -> RecommendedAppsScreen(onBack = { screen = Screen.SETTINGS })
|
||||
Screen.CAR_PREVIEW -> CarPreviewScreen(onBack = { screen = Screen.HOME })
|
||||
Screen.HELP -> HelpScreen(
|
||||
onBack = { screen = Screen.HOME },
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
package app.seb3thehacker.gearslip.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
|
||||
/** A free app that runs on Gearslip's car screen, and where to get it. */
|
||||
private class RecommendedApp(val name: String, val packageName: String, val url: String, val about: String)
|
||||
|
||||
private val recommended = listOf(
|
||||
"Maps" to listOf(
|
||||
RecommendedApp("Vela Maps", "app.vela", "https://github.com/PimpinPumpkin/Vela", "Turn-by-turn navigation without Google."),
|
||||
RecommendedApp("Organic Maps", "app.organicmaps", "https://github.com/organicmaps/organicmaps", "Offline maps and routing."),
|
||||
),
|
||||
"Music" to listOf(
|
||||
RecommendedApp("ViVi Music", "com.vivi.vivimusic", "https://github.com/vivizzz007/vivi-music", "A YouTube Music player."),
|
||||
RecommendedApp("Metrolist", "com.metrolist.music", "https://github.com/MetrolistGroup/Metrolist", "A YouTube Music player."),
|
||||
),
|
||||
)
|
||||
|
||||
/** Opened from Settings: the same list the setup guide ends with. */
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun RecommendedAppsScreen(onBack: () -> Unit) {
|
||||
Scaffold(
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Recommended apps") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(horizontal = 24.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Text(
|
||||
RECOMMENDED_INTRO,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(Modifier.height(24.dp))
|
||||
RecommendedAppsList()
|
||||
Spacer(Modifier.height(24.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal const val RECOMMENDED_INTRO =
|
||||
"These free apps run on Gearslip's car screen. Each button opens the app's page on GitHub."
|
||||
|
||||
/** The same green as the launcher's "works" check. */
|
||||
private val INSTALLED_GREEN = Color(0xFF43A047)
|
||||
|
||||
/** The apps by section, each with a GitHub button. Installed ones say so in green. */
|
||||
@Composable
|
||||
internal fun RecommendedAppsList(modifier: Modifier = Modifier) {
|
||||
val context = LocalContext.current
|
||||
Column(modifier, verticalArrangement = Arrangement.spacedBy(20.dp)) {
|
||||
for ((section, apps) in recommended) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text(
|
||||
section,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
for (app in apps) AppRow(app) { openPage(context, app.url) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppRow(app: RecommendedApp, onOpen: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val icon = remember(app.packageName) { installedIcon(context, app.packageName) }
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
Modifier.size(44.dp).clip(CircleShape).background(MaterialTheme.colorScheme.surfaceContainerHigh),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (icon != null) {
|
||||
Image(icon.toBitmap(96, 96).asImageBitmap(), contentDescription = null, Modifier.fillMaxSize())
|
||||
} else {
|
||||
Text(app.name.take(1), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
Column(Modifier.weight(1f).padding(horizontal = 14.dp)) {
|
||||
Text(app.name, style = MaterialTheme.typography.titleMedium)
|
||||
Text(app.about, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
if (icon != null) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 4.dp)) {
|
||||
Icon(Icons.Filled.CheckCircle, contentDescription = null, tint = INSTALLED_GREEN, modifier = Modifier.size(16.dp))
|
||||
Text(
|
||||
"Installed",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = INSTALLED_GREEN,
|
||||
modifier = Modifier.padding(start = 6.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
OutlinedButton(onClick = onOpen) { Text("GitHub") }
|
||||
}
|
||||
}
|
||||
|
||||
private fun installedIcon(context: Context, packageName: String): Drawable? =
|
||||
runCatching { context.packageManager.getApplicationIcon(packageName) }.getOrNull()
|
||||
|
||||
private fun openPage(context: Context, url: String) {
|
||||
runCatching {
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ import kotlinx.coroutines.withContext
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(onBack: () -> Unit) {
|
||||
fun SettingsScreen(onBack: () -> Unit, onOpenRecommendedApps: () -> Unit) {
|
||||
Scaffold(
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
topBar = {
|
||||
@@ -78,6 +78,7 @@ fun SettingsScreen(onBack: () -> Unit) {
|
||||
) {
|
||||
Spacer(Modifier.height(0.dp))
|
||||
GearslipEnabledSection()
|
||||
AppsSection(onOpenRecommendedApps)
|
||||
CertificateSection()
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
@@ -119,6 +120,16 @@ private fun GearslipEnabledSection() {
|
||||
}
|
||||
}
|
||||
|
||||
@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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CertificateSection() {
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -61,6 +61,8 @@ private class GuidePage(
|
||||
val command: String? = null,
|
||||
val actionLabel: String? = null,
|
||||
val onAction: (() -> Unit)? = null,
|
||||
/** Drawn under [explanation], inside the card. */
|
||||
val content: (@Composable () -> Unit)? = null,
|
||||
)
|
||||
|
||||
/** True once every permission in [permissions] is already granted - nothing left for this page to do. */
|
||||
@@ -261,6 +263,15 @@ 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() },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -336,6 +347,7 @@ fun SetupGuideScreen(onRequestCallScreening: () -> Unit, onDone: () -> Unit) {
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
page.command?.let { CopyableCommand(it) }
|
||||
page.content?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,7 +384,9 @@ fun SetupGuideScreen(onRequestCallScreening: () -> Unit, onDone: () -> Unit) {
|
||||
}
|
||||
else -> {
|
||||
PrimaryRow(showBack = index > 0, onBack = { index -= 1 }) {
|
||||
Button(onClick = { index += 1 }, modifier = it) { Text("Next") }
|
||||
Button(onClick = { index += 1 }, modifier = it) {
|
||||
Text(if (index == pages.lastIndex) "Done" else "Next")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user