mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-11 08:12:26 +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
|
"in.krosbits.musicolet", // Musicolet: browse, play, seek, lyrics
|
||||||
"com.kododake.aabrowser", // Aardvark: browser template, starts, draws, takes touches
|
"com.kododake.aabrowser", // Aardvark: browser template, starts, draws, takes touches
|
||||||
"com.vivi.vivimusic", // Vivi Music: browse, play, seek, works well
|
"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
|
"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. */
|
/** Shown on the launcher with a yellow mark: starts and can be used, but not cleanly enough to call working. */
|
||||||
private val partial = setOf(
|
private val partial = setOf(
|
||||||
"app.vela", // Vela Maps: connects and draws, but rough enough not to call it working yet
|
"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
|
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.BuildConfig
|
||||||
import app.seb3thehacker.gearslip.CertProvider
|
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. */
|
/** Three screens and a back stack of depth one: no navigation library needed. */
|
||||||
@Composable
|
@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.
|
// Home stays upright; every other screen turns with the phone.
|
||||||
LaunchedEffect(screen) {
|
LaunchedEffect(screen) {
|
||||||
@@ -98,7 +100,11 @@ fun GearslipApp(onDisconnect: () -> Unit, onRequestCallScreening: () -> Unit) {
|
|||||||
)
|
)
|
||||||
Screen.WHATS_NEW -> WhatsNewScreen(onBack = { screen = Screen.HOME })
|
Screen.WHATS_NEW -> WhatsNewScreen(onBack = { screen = Screen.HOME })
|
||||||
Screen.LOGS -> LogsScreen(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.CAR_PREVIEW -> CarPreviewScreen(onBack = { screen = Screen.HOME })
|
||||||
Screen.HELP -> HelpScreen(
|
Screen.HELP -> HelpScreen(
|
||||||
onBack = { screen = Screen.HOME },
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(onBack: () -> Unit) {
|
fun SettingsScreen(onBack: () -> Unit, onOpenRecommendedApps: () -> Unit) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
containerColor = MaterialTheme.colorScheme.background,
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -78,6 +78,7 @@ fun SettingsScreen(onBack: () -> Unit) {
|
|||||||
) {
|
) {
|
||||||
Spacer(Modifier.height(0.dp))
|
Spacer(Modifier.height(0.dp))
|
||||||
GearslipEnabledSection()
|
GearslipEnabledSection()
|
||||||
|
AppsSection(onOpenRecommendedApps)
|
||||||
CertificateSection()
|
CertificateSection()
|
||||||
Spacer(Modifier.height(16.dp))
|
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
|
@Composable
|
||||||
private fun CertificateSection() {
|
private fun CertificateSection() {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ private class GuidePage(
|
|||||||
val command: String? = null,
|
val command: String? = null,
|
||||||
val actionLabel: String? = null,
|
val actionLabel: String? = null,
|
||||||
val onAction: (() -> Unit)? = 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. */
|
/** 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,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
page.command?.let { CopyableCommand(it) }
|
page.command?.let { CopyableCommand(it) }
|
||||||
|
page.content?.invoke()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,7 +384,9 @@ fun SetupGuideScreen(onRequestCallScreening: () -> Unit, onDone: () -> Unit) {
|
|||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
PrimaryRow(showBack = index > 0, onBack = { index -= 1 }) {
|
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