mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
Apps can say they work with Gearslip, and each recommended app gets its own page
An app adds one meta-data line to its manifest and Gearslip marks it Works with Gearslip in the Recommended apps list. Tapping an app opens its page, with an Open or Get button. A README badge for app developers is in docs/badge, documented in BUILDING_APPS.md.
This commit is contained in:
1 parent
e72e99d699
commit
3d3e7669ac
7 files changed
+352
-19
No files matched your search
@@ -469,6 +469,44 @@ home-screen pane comes later. Anything that needs its own rendering belongs in a
|
||||
|
||||
---
|
||||
|
||||
## Works with Gearslip
|
||||
|
||||
Once your app runs well on Gearslip, say so in two places: your manifest and your README.
|
||||
|
||||
### In your manifest
|
||||
|
||||
Add these lines to `<application>`:
|
||||
|
||||
```xml
|
||||
<meta-data
|
||||
android:name="app.seb3thehacker.gearslip.works_with"
|
||||
android:value="true" />
|
||||
<meta-data
|
||||
android:name="app.seb3thehacker.gearslip.about"
|
||||
android:resource="@string/gearslip_about" />
|
||||
```
|
||||
|
||||
Gearslip then marks your app "Works with Gearslip" in its Recommended apps list and on your app's
|
||||
page in Gearslip. An installed app with the flag gets its own place at the top of the list.
|
||||
|
||||
The second entry is optional. It gives your page one sentence on what your app does in the car,
|
||||
such as "Spots trackers and cameras nearby." Write it in place with `android:value` if you have no
|
||||
string resource for it.
|
||||
|
||||
The flag is a label. Gearslip finds and runs your app the same way with or without it.
|
||||
|
||||
### In your README
|
||||
|
||||
Add the badge:
|
||||
|
||||
```markdown
|
||||
[](https://github.com/twinksoftware/gearslip)
|
||||
```
|
||||
|
||||
It matches the flat badges most READMEs already show.
|
||||
|
||||
---
|
||||
|
||||
## Further reading
|
||||
|
||||
- [README.md](../README.md) - what Gearslip is.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="147" height="20" role="img" aria-label="Works with Gearslip">
|
||||
<title>Works with Gearslip</title>
|
||||
<clipPath id="r"><rect width="147" height="20" rx="3" fill="#fff"/></clipPath>
|
||||
<g clip-path="url(#r)">
|
||||
<rect width="88" height="20" fill="#555"/>
|
||||
<rect x="88" width="59" height="20" fill="#8C1F1F"/>
|
||||
</g>
|
||||
<g transform="translate(5 3.5) scale(0.228) translate(-29.5 -25.5)" fill="#fff">
|
||||
<path fill-rule="evenodd" d="M54 36.18 58.64 30.7 67.2 34.24 66.6 41.4 71.82 54 77.3 58.64 73.76 67.2 66.6 66.6 67.2 73.76 58.64 77.3 54 71.82 49.36 77.3 40.8 73.76 41.4 66.6 34.24 67.2 30.7 58.64 36.18 54 30.7 49.36 34.24 40.8 41.4 41.4 40.8 34.24 49.36 30.7ZM54 46.08A7.92 7.92 0 1 1 53.99 46.08Z"/>
|
||||
<path d="M78.61 40.7 85.31 43.29 84.74 52.55 77.78 54.31Z"/>
|
||||
</g>
|
||||
<g fill="#fff" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11" text-rendering="geometricPrecision">
|
||||
<text x="22" y="14" textLength="60">works with</text>
|
||||
<text x="94" y="14" textLength="47">Gearslip</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,167 @@
|
||||
package app.seb3thehacker.gearslip.ui
|
||||
|
||||
import android.content.Intent
|
||||
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.shape.RoundedCornerShape
|
||||
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.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
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.asImageBitmap
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import app.seb3thehacker.gearslip.R
|
||||
|
||||
/** One app as its page shows it. [url] is where to get it; apps found by their flag have none. */
|
||||
class PageApp(val name: String, val packageName: String, val url: String?, val about: String?)
|
||||
|
||||
/**
|
||||
* One app's page, over whatever opened it: its icon, its own sentence, the Works with Gearslip
|
||||
* mark when it declares the flag, and one button, to open it or to get it.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AppPage(app: PageApp, onDismiss: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val icon = remember(app.packageName) { runCatching { context.packageManager.getApplicationIcon(app.packageName) }.getOrNull() }
|
||||
val declares = remember(app.packageName) { WorksWith.declares(context.packageManager, app.packageName) }
|
||||
val about = remember(app.packageName) { app.about ?: WorksWith.about(context.packageManager, app.packageName) }
|
||||
val installed = icon != null
|
||||
Dialog(onDismissRequest = onDismiss, properties = DialogProperties(usePlatformDefaultWidth = false)) {
|
||||
Scaffold(
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onDismiss) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(horizontal = 24.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Box(
|
||||
Modifier.size(96.dp).clip(CircleShape).background(MaterialTheme.colorScheme.surfaceContainerHigh),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (icon != null) {
|
||||
Image(icon.toBitmap(192, 192).asImageBitmap(), contentDescription = null, Modifier.fillMaxSize())
|
||||
} else {
|
||||
Text(app.name.take(1), style = MaterialTheme.typography.headlineLarge, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text(app.name, style = MaterialTheme.typography.headlineSmall, color = MaterialTheme.colorScheme.onSurface, textAlign = TextAlign.Center)
|
||||
if (declares) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
WorksWithMark()
|
||||
}
|
||||
if (about != null) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text(about, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center)
|
||||
}
|
||||
Spacer(Modifier.height(24.dp))
|
||||
if (installed) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(Icons.Filled.CheckCircle, contentDescription = null, tint = INSTALLED_GREEN, modifier = Modifier.size(18.dp))
|
||||
Text(
|
||||
stringResource(R.string.recommended_installed),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = INSTALLED_GREEN,
|
||||
modifier = Modifier.padding(start = 6.dp),
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
stringResource(R.string.app_page_on_car),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
val launch = remember(app.packageName) { context.packageManager.getLaunchIntentForPackage(app.packageName) }
|
||||
if (launch != null) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Button(onClick = { runCatching { context.startActivity(launch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) } }, Modifier.fillMaxWidth()) {
|
||||
Text(stringResource(R.string.app_page_open, app.name))
|
||||
}
|
||||
}
|
||||
if (app.url != null) {
|
||||
TextButton(onClick = { openPage(context, app.url) }) { Text(stringResource(R.string.app_page_github)) }
|
||||
}
|
||||
} else if (app.url != null) {
|
||||
Text(
|
||||
stringResource(R.string.app_page_get_hint),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Button(onClick = { openPage(context, app.url) }, Modifier.fillMaxWidth()) {
|
||||
Text(stringResource(R.string.app_page_get, app.name))
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(24.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The Works with Gearslip mark: the logo's gear and the words, on a tinted pill. */
|
||||
@Composable
|
||||
fun WorksWithMark(modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier
|
||||
.clip(RoundedCornerShape(50))
|
||||
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||
.padding(start = 8.dp, end = 10.dp, top = 3.dp, bottom = 3.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp),
|
||||
) {
|
||||
Icon(painterResource(R.drawable.works_with_gear), contentDescription = null, tint = MaterialTheme.colorScheme.onPrimaryContainer, modifier = Modifier.size(14.dp))
|
||||
Text(stringResource(R.string.works_with_gearslip), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onPrimaryContainer)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -34,7 +35,10 @@ import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -105,32 +109,60 @@ fun RecommendedAppsScreen(onBack: () -> Unit) {
|
||||
internal const val SHOW_RECOMMENDED_APPS = false
|
||||
|
||||
/** The same green as the launcher's "works" check. */
|
||||
private val INSTALLED_GREEN = Color(0xFF43A047)
|
||||
internal val INSTALLED_GREEN = Color(0xFF43A047)
|
||||
|
||||
/** The apps by section, each with a GitHub button. Installed ones say so in green. */
|
||||
/**
|
||||
* The apps by section, each with a GitHub button; tapping the rest of a row opens the app's page.
|
||||
* Installed ones say so in green. Installed apps that declare the Works with Gearslip flag come
|
||||
* first, under their own heading, unless the list already has them.
|
||||
*/
|
||||
@Composable
|
||||
internal fun RecommendedAppsList(modifier: Modifier = Modifier) {
|
||||
val context = LocalContext.current
|
||||
var page by remember { mutableStateOf<PageApp?>(null) }
|
||||
val declared = remember {
|
||||
val listed = recommended.flatMap { (_, apps) -> apps.map { it.packageName } }.toSet()
|
||||
WorksWith.installed(context).filter { it.packageName !in listed }
|
||||
}
|
||||
Column(modifier, verticalArrangement = Arrangement.spacedBy(20.dp)) {
|
||||
for ((section, apps) in recommended) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text(
|
||||
stringResource(section),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
for (app in apps) AppRow(app) { openPage(context, app.url) }
|
||||
if (declared.isNotEmpty()) {
|
||||
Section(stringResource(R.string.works_with_gearslip)) {
|
||||
for (app in declared) {
|
||||
AppRow(app.label, app.packageName, app.about, onPage = { page = PageApp(app.label, app.packageName, null, app.about) }, onGitHub = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
for ((section, apps) in recommended) {
|
||||
Section(stringResource(section)) {
|
||||
for (app in apps) {
|
||||
val text = stringResource(app.about)
|
||||
AppRow(app.name, app.packageName, text, onPage = { page = PageApp(app.name, app.packageName, app.url, text) }, onGitHub = { openPage(context, app.url) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
page?.let { AppPage(it, onDismiss = { page = null }) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Section(title: String, content: @Composable () -> Unit) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Text(
|
||||
title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AppRow(app: RecommendedApp, onOpen: () -> Unit) {
|
||||
private fun AppRow(name: String, packageName: String, about: String?, onPage: () -> Unit, onGitHub: (() -> Unit)?) {
|
||||
val context = LocalContext.current
|
||||
val icon = remember(app.packageName) { installedIcon(context, app.packageName) }
|
||||
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
val icon = remember(packageName) { installedIcon(context, packageName) }
|
||||
val declares = remember(packageName) { WorksWith.declares(context.packageManager, packageName) }
|
||||
Row(Modifier.fillMaxWidth().clip(MaterialTheme.shapes.medium).clickable(onClick = onPage), verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
Modifier.size(44.dp).clip(CircleShape).background(MaterialTheme.colorScheme.surfaceContainerHigh),
|
||||
contentAlignment = Alignment.Center,
|
||||
@@ -138,14 +170,15 @@ private fun AppRow(app: RecommendedApp, onOpen: () -> Unit) {
|
||||
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, color = MaterialTheme.colorScheme.onSurface)
|
||||
Text(name.take(1), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
}
|
||||
Column(Modifier.weight(1f).padding(horizontal = 14.dp)) {
|
||||
// Colours set outright: the setup guide draws this on its own background, with no
|
||||
// Surface to hand down a content colour, and unset text came out dark on dark.
|
||||
Text(app.name, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface)
|
||||
Text(stringResource(app.about), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Text(name, style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface)
|
||||
if (about != null) Text(about, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
if (declares) WorksWithMark(Modifier.padding(top = 4.dp))
|
||||
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))
|
||||
@@ -158,14 +191,14 @@ private fun AppRow(app: RecommendedApp, onOpen: () -> Unit) {
|
||||
}
|
||||
}
|
||||
}
|
||||
OutlinedButton(onClick = onOpen) { Text("GitHub") }
|
||||
if (onGitHub != null) OutlinedButton(onClick = onGitHub) { Text("GitHub") }
|
||||
}
|
||||
}
|
||||
|
||||
private fun installedIcon(context: Context, packageName: String): Drawable? =
|
||||
runCatching { context.packageManager.getApplicationIcon(packageName) }.getOrNull()
|
||||
|
||||
private fun openPage(context: Context, url: String) {
|
||||
internal fun openPage(context: Context, url: String) {
|
||||
runCatching {
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package app.seb3thehacker.gearslip.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
|
||||
/**
|
||||
* Apps that say they work with Gearslip, by a line in their manifest's `<application>`:
|
||||
*
|
||||
* ```xml
|
||||
* <meta-data android:name="app.seb3thehacker.gearslip.works_with" android:value="true" />
|
||||
* <meta-data android:name="app.seb3thehacker.gearslip.about" android:resource="@string/gearslip_about" />
|
||||
* ```
|
||||
*
|
||||
* The second line is optional: one sentence for the app's page in Gearslip. Gearslip already sees
|
||||
* every app with a launcher icon (the manifest's MAIN/LAUNCHER query), so reading these needs
|
||||
* nothing more.
|
||||
*/
|
||||
object WorksWith {
|
||||
|
||||
const val FLAG = "app.seb3thehacker.gearslip.works_with"
|
||||
const val ABOUT = "app.seb3thehacker.gearslip.about"
|
||||
|
||||
class Declared(val packageName: String, val label: String, val about: String?)
|
||||
|
||||
private fun meta(manager: PackageManager, pkg: String): Bundle? =
|
||||
runCatching { manager.getApplicationInfo(pkg, PackageManager.GET_META_DATA).metaData }.getOrNull()
|
||||
|
||||
fun declares(manager: PackageManager, pkg: String): Boolean = meta(manager, pkg)?.getBoolean(FLAG) == true
|
||||
|
||||
/** The app's own sentence about itself, from a string resource or written in place. */
|
||||
fun about(manager: PackageManager, pkg: String): String? {
|
||||
val meta = meta(manager, pkg) ?: return null
|
||||
val res = meta.getInt(ABOUT)
|
||||
if (res != 0) return runCatching { manager.getResourcesForApplication(pkg).getString(res) }.getOrNull()
|
||||
return meta.getString(ABOUT)?.takeIf { it.isNotBlank() }
|
||||
}
|
||||
|
||||
/** Every installed app that declares the flag, by name. */
|
||||
fun installed(context: Context): List<Declared> {
|
||||
val manager = context.packageManager
|
||||
val launcher = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER)
|
||||
return manager.queryIntentActivities(launcher, 0)
|
||||
.map { it.activityInfo.packageName }
|
||||
.distinct()
|
||||
.filter { it != context.packageName && declares(manager, it) }
|
||||
.map { pkg ->
|
||||
val label = runCatching { manager.getApplicationInfo(pkg, 0).loadLabel(manager).toString() }.getOrDefault(pkg)
|
||||
Declared(pkg, label, about(manager, pkg))
|
||||
}
|
||||
.sortedBy { it.label.lowercase() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- The launcher icon's gear, cropped to its edges, for the "Works with Gearslip" mark. -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="57"
|
||||
android:viewportHeight="57">
|
||||
<group android:translateX="-29.5" android:translateY="-25.5">
|
||||
<path
|
||||
android:pathData="M 54.00,36.18 L 58.64,30.70 L 67.20,34.24 L 66.60,41.40 L 71.82,54.00 L 77.30,58.64 L 73.76,67.20 L 66.60,66.60 L 66.60,66.60 L 67.20,73.76 L 58.64,77.30 L 54.00,71.82 L 54.00,71.82 L 49.36,77.30 L 40.80,73.76 L 41.40,66.60 L 41.40,66.60 L 34.24,67.20 L 30.70,58.64 L 36.18,54.00 L 36.18,54.00 L 30.70,49.36 L 34.24,40.80 L 41.40,41.40 L 41.40,41.40 L 40.80,34.24 L 49.36,30.70 L 54.00,36.18 Z M54,46.08 A7.92,7.92 0 1,1 53.99,46.08 Z"
|
||||
android:fillType="evenOdd"
|
||||
android:fillColor="#FF000000"/>
|
||||
<path
|
||||
android:pathData="M 78.61,40.70 L 85.31,43.29 L 84.74,52.55 L 77.78,54.31 Z"
|
||||
android:fillColor="#FF000000"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -96,6 +96,14 @@
|
||||
<string name="recommended_about_ytm">A YouTube Music player.</string>
|
||||
<string name="recommended_about_antennapod">Podcasts, chapters and all.</string>
|
||||
<string name="recommended_about_rfsentinel">Spots trackers and cameras nearby.</string>
|
||||
<!-- The mark on apps that declare they work with Gearslip, and the list's heading for them. -->
|
||||
<string name="works_with_gearslip">Works with Gearslip</string>
|
||||
<!-- One app's page, opened from Recommended apps. %1$s is the app's name. -->
|
||||
<string name="app_page_on_car">Gearslip shows it on the car screen.</string>
|
||||
<string name="app_page_get_hint">Install it, and Gearslip shows it on the car screen.</string>
|
||||
<string name="app_page_open">Open %1$s</string>
|
||||
<string name="app_page_get">Get %1$s</string>
|
||||
<string name="app_page_github">View on GitHub</string>
|
||||
|
||||
<!-- Settings -->
|
||||
<string name="settings_title">Settings</string>
|
||||
|
||||
Reference in new issue
Block a user