mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-11 00:12:24 +01:00
voice: saved places, so "navigate home" or "take me to work" works in any map app
Home, Work and three named places live in the Car card on Home, under Saved places. Type an address or save where you're standing.
This commit is contained in:
1 parent
57803988d4
commit
2f49559600
22 files changed
+579
-3
No files matched your search
@@ -95,7 +95,12 @@ object AssistantCommands {
|
||||
heard: String,
|
||||
): String? {
|
||||
// "use Organic Maps to get to X", "open OsmAnd and navigate to X": the app comes first.
|
||||
var text = heard
|
||||
// "take me home", "go home": no "to", so no trigger would catch them.
|
||||
var text = if (Regex("^(?:(?:take|bring|get|drive) me|go|drive|head|get|directions|go back|head back) home$").matches(heard)) {
|
||||
"navigate to home"
|
||||
} else {
|
||||
heard
|
||||
}
|
||||
var leadingApp: String? = null
|
||||
Regex("^(?:use|using|with|open|in|on) (.+?) (?:to |and )(.+)$").find(text)?.let { m ->
|
||||
if (after(m.groupValues[2], NAVIGATE_TRIGGERS) != null) {
|
||||
@@ -135,6 +140,20 @@ object AssistantCommands {
|
||||
val (place, nearby) = plainPlace(destination)
|
||||
destination = place
|
||||
if (destination.isBlank()) return "Where to?"
|
||||
// "home", "work", or a name from Saved places: its address is the search.
|
||||
var spoken: String? = null
|
||||
SavedPlaces.match(context, destination)?.let { (slot, saved) ->
|
||||
val name = when (slot) {
|
||||
SavedPlaces.HOME -> "home"
|
||||
SavedPlaces.WORK -> "work"
|
||||
else -> saved.label
|
||||
}
|
||||
if (saved.address.isBlank()) {
|
||||
return "You haven't saved a $name address yet. Add it under Saved places in Gearslip's settings on your phone."
|
||||
}
|
||||
destination = saved.address
|
||||
spoken = if (slot == SavedPlaces.HOME) "home" else "to $name"
|
||||
}
|
||||
|
||||
val chosen = chosenApp ?: when {
|
||||
navApps.size == 1 -> navApps.first()
|
||||
@@ -144,6 +163,7 @@ object AssistantCommands {
|
||||
|
||||
CarServices.connectNav(template, frame, destination)
|
||||
navigator.home()
|
||||
spoken?.let { return "Navigating $it with ${chosen.label}." }
|
||||
return if (nearby) "Finding the nearest $destination with ${chosen.label}." else "Navigating to $destination with ${chosen.label}."
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package app.seb3thehacker.gearslip.car
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Geocoder
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import app.seb3thehacker.gearslip.AppSettings
|
||||
import app.seb3thehacker.gearslip.GearslipLog
|
||||
import java.util.Locale
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
/** One saved place. Home and Work keep a blank [label]; their names come from strings.xml. */
|
||||
data class SavedPlace(val label: String = "", val address: String = "")
|
||||
|
||||
/**
|
||||
* Up to [COUNT] places a driver can name by voice in any map app: slot [HOME], slot [WORK],
|
||||
* then three they name themselves. "Navigate home" sends the saved address as the search.
|
||||
*/
|
||||
object SavedPlaces {
|
||||
|
||||
const val COUNT = 5
|
||||
const val HOME = 0
|
||||
const val WORK = 1
|
||||
|
||||
private const val KEY = "saved_places"
|
||||
|
||||
private val HOME_WORDS = setOf("home", "house", "place", "home address", "my home", "my house", "my place")
|
||||
private val WORK_WORDS = setOf("work", "office", "job", "work address", "my work", "my office", "my job", "the office")
|
||||
|
||||
fun all(context: Context): List<SavedPlace> {
|
||||
val saved = runCatching {
|
||||
val array = JSONArray(AppSettings.getString(context, KEY, "[]"))
|
||||
List(array.length()) { i ->
|
||||
array.getJSONObject(i).let { SavedPlace(it.optString("label"), it.optString("address")) }
|
||||
}
|
||||
}.getOrDefault(emptyList())
|
||||
return List(COUNT) { saved.getOrNull(it) ?: SavedPlace() }
|
||||
}
|
||||
|
||||
fun set(context: Context, index: Int, place: SavedPlace) {
|
||||
val next = all(context).toMutableList().also { it[index] = place }
|
||||
val array = JSONArray()
|
||||
next.forEach { array.put(JSONObject().put("label", it.label).put("address", it.address)) }
|
||||
AppSettings.putString(context, KEY, array.toString())
|
||||
}
|
||||
|
||||
/**
|
||||
* The slot [spoken] names, if any: "home", "my house", "work", or a custom label. Returns the
|
||||
* slot even when its address is blank, so the assistant can say it isn't set yet.
|
||||
*/
|
||||
fun match(context: Context, spoken: String): Pair<Int, SavedPlace>? {
|
||||
val said = spoken.lowercase().trim()
|
||||
val places = all(context)
|
||||
if (said in HOME_WORDS) return HOME to places[HOME]
|
||||
if (said in WORK_WORDS) return WORK to places[WORK]
|
||||
val squashed = squash(said.removePrefix("my ").removePrefix("the "))
|
||||
if (squashed.isEmpty()) return null
|
||||
places.forEachIndexed { i, place ->
|
||||
if (i > WORK && place.label.isNotBlank() && squash(place.label) == squashed) return i to place
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun squash(s: String) = s.lowercase().filter { it.isLetterOrDigit() }
|
||||
|
||||
fun hasLocationPermission(context: Context) =
|
||||
context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
|
||||
context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
||||
|
||||
/** Where the phone is now, as a street address, or as coordinates when there's no geocoder. Null if unknown. */
|
||||
suspend fun here(context: Context): String? = withContext(Dispatchers.IO) {
|
||||
val location = locate(context) ?: return@withContext null
|
||||
val address = runCatching {
|
||||
if (!Geocoder.isPresent()) return@runCatching null
|
||||
@Suppress("DEPRECATION")
|
||||
Geocoder(context, Locale.getDefault()).getFromLocation(location.latitude, location.longitude, 1)
|
||||
?.firstOrNull()?.getAddressLine(0)
|
||||
}.getOrNull()
|
||||
address ?: "%.6f,%.6f".format(Locale.US, location.latitude, location.longitude)
|
||||
}
|
||||
|
||||
private suspend fun locate(context: Context): Location? {
|
||||
if (!hasLocationPermission(context)) return null
|
||||
val manager = context.getSystemService(LocationManager::class.java) ?: return null
|
||||
val provider = listOf(LocationManager.FUSED_PROVIDER, LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER)
|
||||
.firstOrNull { it in manager.allProviders && manager.isProviderEnabled(it) }
|
||||
// A fresh fix: saving the spot you're standing on wants better than an hour-old one.
|
||||
val fresh = provider?.let {
|
||||
withTimeoutOrNull(FIX_TIMEOUT_MS) {
|
||||
suspendCancellableCoroutine { done ->
|
||||
runCatching {
|
||||
manager.getCurrentLocation(it, null, context.mainExecutor) { fix -> done.resume(fix) }
|
||||
}.onFailure { e ->
|
||||
GearslipLog.w("places: fix failed: ${e.message}")
|
||||
done.resume(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return fresh ?: manager.allProviders
|
||||
.mapNotNull { runCatching { manager.getLastKnownLocation(it) }.getOrNull() }
|
||||
.maxByOrNull { it.time }
|
||||
}
|
||||
|
||||
private const val FIX_TIMEOUT_MS = 15_000L
|
||||
}
|
||||
@@ -43,7 +43,7 @@ import app.seb3thehacker.gearslip.BuildConfig
|
||||
import app.seb3thehacker.gearslip.CertProvider
|
||||
import app.seb3thehacker.gearslip.stats.UsageStats
|
||||
|
||||
private enum class Screen { HOME, LOGS, SETTINGS, CAR_PREVIEW, CERT_SETUP, SETUP_GUIDE, USAGE_NOTES, HELP, WHATS_NEW, RECOMMENDED_APPS, CAR_GROUP }
|
||||
private enum class Screen { HOME, LOGS, SETTINGS, CAR_PREVIEW, CERT_SETUP, SETUP_GUIDE, USAGE_NOTES, HELP, WHATS_NEW, RECOMMENDED_APPS, CAR_GROUP, SAVED_PLACES }
|
||||
|
||||
/** Three screens and a back stack of depth one: no navigation library needed. */
|
||||
@Composable
|
||||
@@ -127,7 +127,13 @@ fun GearslipApp(onDisconnect: () -> Unit, onRequestCallScreening: () -> Unit) {
|
||||
onOpenCarPreview = { backTo = Screen.HOME; screen = Screen.CAR_PREVIEW },
|
||||
onOpenLogs = { backTo = Screen.HOME; screen = Screen.LOGS },
|
||||
onDisconnect = onDisconnect,
|
||||
onOpenCarGroup = { carGroup = it; screen = Screen.CAR_GROUP },
|
||||
onOpenCarGroup = {
|
||||
if (it == CarGroup.PLACES) {
|
||||
screen = Screen.SAVED_PLACES
|
||||
} else {
|
||||
carGroup = it; screen = Screen.CAR_GROUP
|
||||
}
|
||||
},
|
||||
)
|
||||
Screen.CAR_GROUP -> CarGroupScreen(carGroup, onBack = { screen = Screen.HOME })
|
||||
Screen.WHATS_NEW -> WhatsNewScreen(onBack = { screen = backTo })
|
||||
@@ -139,6 +145,7 @@ fun GearslipApp(onDisconnect: () -> Unit, onRequestCallScreening: () -> Unit) {
|
||||
atUsageNotes = settingsAtUsageNotes,
|
||||
)
|
||||
Screen.RECOMMENDED_APPS -> RecommendedAppsScreen(onBack = { screen = Screen.SETTINGS })
|
||||
Screen.SAVED_PLACES -> SavedPlacesScreen(onBack = { screen = Screen.HOME })
|
||||
Screen.CAR_PREVIEW -> CarPreviewScreen(onBack = { screen = backTo })
|
||||
Screen.HELP -> HelpScreen(
|
||||
onBack = { screen = backTo },
|
||||
|
||||
@@ -46,6 +46,7 @@ enum class CarGroup(@param:StringRes val title: Int) {
|
||||
SCREEN(R.string.car_group_screen),
|
||||
NAV_BAR(R.string.car_group_nav_bar),
|
||||
SOUND(R.string.car_group_sound),
|
||||
PLACES(R.string.places_title),
|
||||
WARNINGS(R.string.car_group_warnings),
|
||||
MORE(R.string.car_group_more),
|
||||
}
|
||||
@@ -98,6 +99,8 @@ internal fun HomeCarSettings(onOpen: (CarGroup) -> Unit, modifier: Modifier = Mo
|
||||
val navBars = navBarChoices()
|
||||
val context = androidx.compose.ui.platform.LocalContext.current
|
||||
fun str(id: Int) = context.getString(id)
|
||||
// Read again each time Home shows, so a place saved a moment ago is listed.
|
||||
val places = androidx.compose.runtime.remember { app.seb3thehacker.gearslip.car.SavedPlaces.all(context) }
|
||||
fun summary(group: CarGroup) = when (group) {
|
||||
CarGroup.SCREEN -> listOf(opens.label(open), sizes.label(scale), themes.label(theme))
|
||||
CarGroup.NAV_BAR -> listOf(navBars.label(navBar), str(if (playerLeft) R.string.summary_player_left else R.string.summary_player_right))
|
||||
@@ -105,6 +108,14 @@ internal fun HomeCarSettings(onOpen: (CarGroup) -> Unit, modifier: Modifier = Mo
|
||||
str(if (pipeAudio) R.string.summary_usb_audio else R.string.summary_bluetooth_audio),
|
||||
str(if (speakDirections) R.string.summary_directions_on else R.string.summary_directions_off),
|
||||
)
|
||||
CarGroup.PLACES -> places.mapIndexedNotNull { i, place ->
|
||||
when {
|
||||
place.address.isBlank() -> null
|
||||
i == app.seb3thehacker.gearslip.car.SavedPlaces.HOME -> str(R.string.places_home)
|
||||
i == app.seb3thehacker.gearslip.car.SavedPlaces.WORK -> str(R.string.places_work)
|
||||
else -> place.label.ifBlank { context.getString(R.string.places_other, i - app.seb3thehacker.gearslip.car.SavedPlaces.WORK) }
|
||||
}
|
||||
}.ifEmpty { listOf(str(R.string.places_sub)) }
|
||||
CarGroup.WARNINGS -> listOf(
|
||||
str(if (bluetoothWarning && !pipeAudio) R.string.summary_bluetooth_on else R.string.summary_bluetooth_off),
|
||||
str(if (locationWarning) R.string.summary_location_on else R.string.summary_location_off),
|
||||
@@ -253,6 +264,7 @@ private fun GroupRows(group: CarGroup) {
|
||||
note = stringResource(R.string.car_phone_speech_note),
|
||||
)
|
||||
}
|
||||
CarGroup.PLACES -> Unit
|
||||
CarGroup.WARNINGS -> {
|
||||
if (!pipeAudio) {
|
||||
Toggle(stringResource(R.string.car_warn_bluetooth), bluetoothWarning, CarSettings::setBluetoothWarning)
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
package app.seb3thehacker.gearslip.ui
|
||||
|
||||
import android.Manifest
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
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.filled.Home
|
||||
import androidx.compose.material.icons.filled.LocationOn
|
||||
import androidx.compose.material.icons.filled.Place
|
||||
import androidx.compose.material.icons.materialIcon
|
||||
import androidx.compose.material.icons.materialPath
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
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.OutlinedTextField
|
||||
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.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
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.res.stringResource
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.unit.dp
|
||||
import app.seb3thehacker.gearslip.R
|
||||
import app.seb3thehacker.gearslip.car.SavedPlace
|
||||
import app.seb3thehacker.gearslip.car.SavedPlaces
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/** Home, Work and three named places, each saved the moment it's typed. */
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SavedPlacesScreen(onBack: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val places = remember { SavedPlaces.all(context).toMutableList() }
|
||||
Scaffold(
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.places_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.imePadding()
|
||||
.padding(horizontal = 24.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.places_intro),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
for (i in 0 until SavedPlaces.COUNT) {
|
||||
PlaceCard(i, places[i]) { place ->
|
||||
places[i] = place
|
||||
SavedPlaces.set(context, i, place)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PlaceCard(index: Int, initial: SavedPlace, onChange: (SavedPlace) -> Unit) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
var label by remember { mutableStateOf(initial.label) }
|
||||
var address by remember { mutableStateOf(initial.address) }
|
||||
var finding by remember { mutableStateOf(false) }
|
||||
var notFound by remember { mutableStateOf(false) }
|
||||
val custom = index > SavedPlaces.WORK
|
||||
|
||||
fun save() = onChange(SavedPlace(if (custom) label.trim() else "", address.trim()))
|
||||
fun findHere() {
|
||||
finding = true
|
||||
notFound = false
|
||||
scope.launch {
|
||||
val here = SavedPlaces.here(context)
|
||||
finding = false
|
||||
if (here == null) {
|
||||
notFound = true
|
||||
} else {
|
||||
address = here
|
||||
save()
|
||||
}
|
||||
}
|
||||
}
|
||||
val askLocation = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
|
||||
if (SavedPlaces.hasLocationPermission(context)) findHere() else notFound = true
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Icon(
|
||||
when (index) {
|
||||
SavedPlaces.HOME -> Icons.Filled.Home
|
||||
SavedPlaces.WORK -> Briefcase
|
||||
else -> Icons.Filled.Place
|
||||
},
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Text(
|
||||
when (index) {
|
||||
SavedPlaces.HOME -> stringResource(R.string.places_home)
|
||||
SavedPlaces.WORK -> stringResource(R.string.places_work)
|
||||
else -> label.ifBlank { stringResource(R.string.places_other, index - SavedPlaces.WORK) }
|
||||
},
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
}
|
||||
if (custom) {
|
||||
OutlinedTextField(
|
||||
value = label,
|
||||
onValueChange = { label = it; save() },
|
||||
label = { Text(stringResource(R.string.places_name)) },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Words, imeAction = ImeAction.Next),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = address,
|
||||
onValueChange = { address = it; notFound = false; save() },
|
||||
label = { Text(stringResource(R.string.places_address)) },
|
||||
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Words, imeAction = ImeAction.Done),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
if (notFound) {
|
||||
Text(
|
||||
stringResource(R.string.places_not_found),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
if (SavedPlaces.hasLocationPermission(context)) {
|
||||
findHere()
|
||||
} else {
|
||||
askLocation.launch(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION))
|
||||
}
|
||||
},
|
||||
enabled = !finding,
|
||||
) {
|
||||
if (finding) {
|
||||
CircularProgressIndicator(Modifier.size(18.dp), strokeWidth = 2.dp)
|
||||
} else {
|
||||
Icon(Icons.Filled.LocationOn, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
}
|
||||
Spacer(Modifier.size(8.dp))
|
||||
Text(stringResource(if (finding) R.string.places_finding else R.string.places_here))
|
||||
}
|
||||
if (address.isNotBlank() || label.isNotBlank()) {
|
||||
TextButton(onClick = { label = ""; address = ""; notFound = false; save() }) {
|
||||
Text(stringResource(R.string.places_clear))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Material's "work" briefcase, which only ships in the extended icon set. */
|
||||
private val Briefcase: ImageVector by lazy {
|
||||
materialIcon("Filled.Work") {
|
||||
materialPath {
|
||||
moveTo(20f, 6f); horizontalLineToRelative(-4f); verticalLineTo(4f)
|
||||
curveToRelative(0f, -1.11f, -0.89f, -2f, -2f, -2f); horizontalLineToRelative(-4f)
|
||||
curveTo(8.89f, 2f, 8f, 2.89f, 8f, 4f); verticalLineToRelative(2f); horizontalLineTo(4f)
|
||||
curveToRelative(-1.11f, 0f, -1.99f, 0.89f, -1.99f, 2f); lineTo(2f, 19f)
|
||||
curveToRelative(0f, 1.11f, 0.89f, 2f, 2f, 2f); horizontalLineToRelative(16f)
|
||||
curveToRelative(1.11f, 0f, 2f, -0.89f, 2f, -2f); verticalLineTo(8f)
|
||||
curveToRelative(0f, -1.11f, -0.89f, -2f, -2f, -2f); close()
|
||||
moveTo(14f, 6f); horizontalLineToRelative(-4f); verticalLineTo(4f); horizontalLineToRelative(4f)
|
||||
verticalLineToRelative(2f); close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -849,4 +849,16 @@
|
||||
<string name="status_projecting_detail">البث إلى وحدة الرأس.</string>
|
||||
<string name="status_disconnected">انقطع الاتصال</string>
|
||||
<string name="status_disconnected_detail">لم تعد وحدة الرأس متصلة.</string>
|
||||
<string name="places_title">الأماكن المحفوظة</string>
|
||||
<string name="places_sub">قل “navigate home” في أي تطبيق خرائط</string>
|
||||
<string name="places_intro">قل “navigate home” أو “take me to work” أو “navigate to” مع اسم اخترته، وسيرسل Gearslip العنوان إلى تطبيق الخرائط. الأوامر الصوتية بالإنجليزية فقط حاليًا.</string>
|
||||
<string name="places_home">المنزل</string>
|
||||
<string name="places_work">العمل</string>
|
||||
<string name="places_other">مكان %1$d</string>
|
||||
<string name="places_name">الاسم</string>
|
||||
<string name="places_address">العنوان</string>
|
||||
<string name="places_here">استخدام موقعي الحالي</string>
|
||||
<string name="places_finding">جارٍ البحث…</string>
|
||||
<string name="places_not_found">لم يعثر Gearslip على موقعك. تأكد من تشغيل الموقع.</string>
|
||||
<string name="places_clear">مسح</string>
|
||||
</resources>
|
||||
@@ -833,4 +833,16 @@
|
||||
<string name="status_projecting_detail">Streamování do autorádia.</string>
|
||||
<string name="status_disconnected">Odpojeno</string>
|
||||
<string name="status_disconnected_detail">Autorádio už není připojené.</string>
|
||||
<string name="places_title">Uložená místa</string>
|
||||
<string name="places_sub">Řekněte „navigate home“ v jakékoli mapové aplikaci</string>
|
||||
<string name="places_intro">Řekněte „navigate home“, „take me to work“ nebo „navigate to“ s vlastním názvem a Gearslip pošle adresu do mapové aplikace. Hlasové příkazy zatím fungují jen anglicky.</string>
|
||||
<string name="places_home">Domov</string>
|
||||
<string name="places_work">Práce</string>
|
||||
<string name="places_other">Místo %1$d</string>
|
||||
<string name="places_name">Název</string>
|
||||
<string name="places_address">Adresa</string>
|
||||
<string name="places_here">Použít mou polohu</string>
|
||||
<string name="places_finding">Hledám…</string>
|
||||
<string name="places_not_found">Gearslip nenašel vaši polohu. Zkontrolujte, že je poloha zapnutá.</string>
|
||||
<string name="places_clear">Vymazat</string>
|
||||
</resources>
|
||||
@@ -695,4 +695,16 @@
|
||||
<string name="status_projecting_detail">Das Bild läuft auf dem Bildschirm des Autos.</string>
|
||||
<string name="status_disconnected">Getrennt</string>
|
||||
<string name="status_disconnected_detail">Das Auto ist nicht mehr verbunden.</string>
|
||||
<string name="places_title">Gespeicherte Orte</string>
|
||||
<string name="places_sub">Sag „navigate home“ in jeder Karten-App</string>
|
||||
<string name="places_intro">Sag „navigate home“, „take me to work“ oder „navigate to“ mit einem eigenen Namen, und Gearslip schickt die Adresse an deine Karten-App. Sprachbefehle gibt es bisher nur auf Englisch.</string>
|
||||
<string name="places_home">Zuhause</string>
|
||||
<string name="places_work">Arbeit</string>
|
||||
<string name="places_other">Ort %1$d</string>
|
||||
<string name="places_name">Name</string>
|
||||
<string name="places_address">Adresse</string>
|
||||
<string name="places_here">Aktuellen Standort nehmen</string>
|
||||
<string name="places_finding">Suche Standort…</string>
|
||||
<string name="places_not_found">Gearslip konnte deinen Standort nicht finden. Ist die Standortermittlung an?</string>
|
||||
<string name="places_clear">Löschen</string>
|
||||
</resources>
|
||||
@@ -695,4 +695,16 @@
|
||||
<string name="status_projecting_detail">Mostrando en la pantalla del coche.</string>
|
||||
<string name="status_disconnected">Desconectado</string>
|
||||
<string name="status_disconnected_detail">El coche ya no está conectado.</string>
|
||||
<string name="places_title">Lugares guardados</string>
|
||||
<string name="places_sub">Di “navigate home” en cualquier app de mapas</string>
|
||||
<string name="places_intro">Di “navigate home”, “take me to work” o “navigate to” con un nombre tuyo, y Gearslip envía esa dirección a tu app de mapas. Por ahora los comandos de voz son solo en inglés.</string>
|
||||
<string name="places_home">Casa</string>
|
||||
<string name="places_work">Trabajo</string>
|
||||
<string name="places_other">Lugar %1$d</string>
|
||||
<string name="places_name">Nombre</string>
|
||||
<string name="places_address">Dirección</string>
|
||||
<string name="places_here">Usar mi ubicación</string>
|
||||
<string name="places_finding">Buscando…</string>
|
||||
<string name="places_not_found">Gearslip no encontró dónde estás. Comprueba que la ubicación esté activada.</string>
|
||||
<string name="places_clear">Borrar</string>
|
||||
</resources>
|
||||
@@ -825,4 +825,16 @@
|
||||
<string name="status_projecting_detail">Diffusion vers l’autoradio.</string>
|
||||
<string name="status_disconnected">Déconnecté</string>
|
||||
<string name="status_disconnected_detail">L’autoradio n’est plus connecté.</string>
|
||||
<string name="places_title">Lieux enregistrés</string>
|
||||
<string name="places_sub">Dites « navigate home » dans n’importe quelle app de cartes</string>
|
||||
<string name="places_intro">Dites « navigate home », « take me to work » ou « navigate to » suivi d’un de vos noms, et Gearslip envoie l’adresse à votre app de cartes. Les commandes vocales ne marchent qu’en anglais pour l’instant.</string>
|
||||
<string name="places_home">Domicile</string>
|
||||
<string name="places_work">Travail</string>
|
||||
<string name="places_other">Lieu %1$d</string>
|
||||
<string name="places_name">Nom</string>
|
||||
<string name="places_address">Adresse</string>
|
||||
<string name="places_here">Utiliser ma position</string>
|
||||
<string name="places_finding">Recherche…</string>
|
||||
<string name="places_not_found">Gearslip n’a pas trouvé votre position. Vérifiez que la localisation est activée.</string>
|
||||
<string name="places_clear">Effacer</string>
|
||||
</resources>
|
||||
@@ -825,4 +825,16 @@
|
||||
<string name="status_projecting_detail">Streaming verso l’autoradio.</string>
|
||||
<string name="status_disconnected">Disconnesso</string>
|
||||
<string name="status_disconnected_detail">L’autoradio non è più connessa.</string>
|
||||
<string name="places_title">Luoghi salvati</string>
|
||||
<string name="places_sub">Di’ “navigate home” in qualsiasi app di mappe</string>
|
||||
<string name="places_intro">Di’ “navigate home”, “take me to work” o “navigate to” con un tuo nome, e Gearslip manda l’indirizzo alla tua app di mappe. Per ora i comandi vocali sono solo in inglese.</string>
|
||||
<string name="places_home">Casa</string>
|
||||
<string name="places_work">Lavoro</string>
|
||||
<string name="places_other">Luogo %1$d</string>
|
||||
<string name="places_name">Nome</string>
|
||||
<string name="places_address">Indirizzo</string>
|
||||
<string name="places_here">Usa la mia posizione</string>
|
||||
<string name="places_finding">Ricerca…</string>
|
||||
<string name="places_not_found">Gearslip non ha trovato la tua posizione. Controlla che la localizzazione sia attiva.</string>
|
||||
<string name="places_clear">Cancella</string>
|
||||
</resources>
|
||||
@@ -809,4 +809,16 @@
|
||||
<string name="status_projecting_detail">ヘッドユニットに映像を送っています。</string>
|
||||
<string name="status_disconnected">切断されました</string>
|
||||
<string name="status_disconnected_detail">ヘッドユニットとの接続が切れました。</string>
|
||||
<string name="places_title">保存した場所</string>
|
||||
<string name="places_sub">どの地図アプリでも「navigate home」と話しかけられます</string>
|
||||
<string name="places_intro">「navigate home」「take me to work」、または「navigate to」と自分で付けた名前を言うと、Gearslip がその住所を地図アプリに送ります。音声コマンドは今のところ英語のみです。</string>
|
||||
<string name="places_home">自宅</string>
|
||||
<string name="places_work">職場</string>
|
||||
<string name="places_other">場所 %1$d</string>
|
||||
<string name="places_name">名前</string>
|
||||
<string name="places_address">住所</string>
|
||||
<string name="places_here">現在地を使う</string>
|
||||
<string name="places_finding">検索中…</string>
|
||||
<string name="places_not_found">現在地が見つかりませんでした。位置情報がオンになっているか確認してください。</string>
|
||||
<string name="places_clear">消去</string>
|
||||
</resources>
|
||||
@@ -809,4 +809,16 @@
|
||||
<string name="status_projecting_detail">헤드 유닛으로 스트리밍하고 있습니다.</string>
|
||||
<string name="status_disconnected">연결 해제됨</string>
|
||||
<string name="status_disconnected_detail">헤드 유닛이 더 이상 연결되어 있지 않습니다.</string>
|
||||
<string name="places_title">저장한 장소</string>
|
||||
<string name="places_sub">어느 지도 앱에서든 “navigate home”이라고 말하세요</string>
|
||||
<string name="places_intro">“navigate home”, “take me to work” 또는 “navigate to”와 직접 붙인 이름을 말하면 Gearslip이 그 주소를 지도 앱으로 보냅니다. 음성 명령은 아직 영어만 됩니다.</string>
|
||||
<string name="places_home">집</string>
|
||||
<string name="places_work">직장</string>
|
||||
<string name="places_other">장소 %1$d</string>
|
||||
<string name="places_name">이름</string>
|
||||
<string name="places_address">주소</string>
|
||||
<string name="places_here">현재 위치 사용</string>
|
||||
<string name="places_finding">찾는 중…</string>
|
||||
<string name="places_not_found">Gearslip이 현재 위치를 찾지 못했습니다. 위치가 켜져 있는지 확인하세요.</string>
|
||||
<string name="places_clear">지우기</string>
|
||||
</resources>
|
||||
@@ -817,4 +817,16 @@
|
||||
<string name="status_projecting_detail">Streamen naar de headunit.</string>
|
||||
<string name="status_disconnected">Verbinding verbroken</string>
|
||||
<string name="status_disconnected_detail">De headunit is niet meer verbonden.</string>
|
||||
<string name="places_title">Opgeslagen plekken</string>
|
||||
<string name="places_sub">Zeg “navigate home” in elke kaart-app</string>
|
||||
<string name="places_intro">Zeg “navigate home”, “take me to work” of “navigate to” met een eigen naam, en Gearslip stuurt het adres naar je kaart-app. Spraakopdrachten werken voorlopig alleen in het Engels.</string>
|
||||
<string name="places_home">Thuis</string>
|
||||
<string name="places_work">Werk</string>
|
||||
<string name="places_other">Plek %1$d</string>
|
||||
<string name="places_name">Naam</string>
|
||||
<string name="places_address">Adres</string>
|
||||
<string name="places_here">Huidige locatie gebruiken</string>
|
||||
<string name="places_finding">Zoeken…</string>
|
||||
<string name="places_not_found">Gearslip kon je locatie niet vinden. Staat locatie aan?</string>
|
||||
<string name="places_clear">Wissen</string>
|
||||
</resources>
|
||||
@@ -833,4 +833,16 @@
|
||||
<string name="status_projecting_detail">Przesyłanie obrazu do radia samochodowego.</string>
|
||||
<string name="status_disconnected">Rozłączono</string>
|
||||
<string name="status_disconnected_detail">Radio samochodowe nie jest już połączone.</string>
|
||||
<string name="places_title">Zapisane miejsca</string>
|
||||
<string name="places_sub">Powiedz „navigate home” w dowolnej aplikacji z mapami</string>
|
||||
<string name="places_intro">Powiedz „navigate home”, „take me to work” albo „navigate to” z własną nazwą, a Gearslip wyśle adres do aplikacji z mapami. Na razie polecenia głosowe działają tylko po angielsku.</string>
|
||||
<string name="places_home">Dom</string>
|
||||
<string name="places_work">Praca</string>
|
||||
<string name="places_other">Miejsce %1$d</string>
|
||||
<string name="places_name">Nazwa</string>
|
||||
<string name="places_address">Adres</string>
|
||||
<string name="places_here">Użyj mojej lokalizacji</string>
|
||||
<string name="places_finding">Szukam…</string>
|
||||
<string name="places_not_found">Gearslip nie znalazł Twojej lokalizacji. Sprawdź, czy lokalizacja jest włączona.</string>
|
||||
<string name="places_clear">Wyczyść</string>
|
||||
</resources>
|
||||
@@ -825,4 +825,16 @@
|
||||
<string name="status_projecting_detail">Transmitindo para a central multimídia.</string>
|
||||
<string name="status_disconnected">Desconectado</string>
|
||||
<string name="status_disconnected_detail">A central multimídia não está mais conectada.</string>
|
||||
<string name="places_title">Lugares salvos</string>
|
||||
<string name="places_sub">Diga “navigate home” em qualquer app de mapas</string>
|
||||
<string name="places_intro">Diga “navigate home”, “take me to work” ou “navigate to” com um nome seu, e o Gearslip envia o endereço para o seu app de mapas. Por enquanto os comandos de voz são só em inglês.</string>
|
||||
<string name="places_home">Casa</string>
|
||||
<string name="places_work">Trabalho</string>
|
||||
<string name="places_other">Lugar %1$d</string>
|
||||
<string name="places_name">Nome</string>
|
||||
<string name="places_address">Endereço</string>
|
||||
<string name="places_here">Usar onde estou</string>
|
||||
<string name="places_finding">Procurando…</string>
|
||||
<string name="places_not_found">O Gearslip não encontrou onde você está. Veja se a localização está ligada.</string>
|
||||
<string name="places_clear">Limpar</string>
|
||||
</resources>
|
||||
@@ -833,4 +833,16 @@
|
||||
<string name="status_projecting_detail">Изображение передаётся на магнитолу.</string>
|
||||
<string name="status_disconnected">Отключено</string>
|
||||
<string name="status_disconnected_detail">Магнитола больше не подключена.</string>
|
||||
<string name="places_title">Сохранённые места</string>
|
||||
<string name="places_sub">Скажите «navigate home» в любом приложении карт</string>
|
||||
<string name="places_intro">Скажите «navigate home», «take me to work» или «navigate to» со своим названием, и Gearslip отправит адрес в приложение карт. Пока голосовые команды работают только на английском.</string>
|
||||
<string name="places_home">Дом</string>
|
||||
<string name="places_work">Работа</string>
|
||||
<string name="places_other">Место %1$d</string>
|
||||
<string name="places_name">Название</string>
|
||||
<string name="places_address">Адрес</string>
|
||||
<string name="places_here">Где я сейчас</string>
|
||||
<string name="places_finding">Ищу…</string>
|
||||
<string name="places_not_found">Gearslip не нашёл, где вы. Проверьте, что геолокация включена.</string>
|
||||
<string name="places_clear">Очистить</string>
|
||||
</resources>
|
||||
@@ -817,4 +817,16 @@
|
||||
<string name="status_projecting_detail">Strömmar till bilstereon.</string>
|
||||
<string name="status_disconnected">Frånkopplad</string>
|
||||
<string name="status_disconnected_detail">Bilstereon är inte längre ansluten.</string>
|
||||
<string name="places_title">Sparade platser</string>
|
||||
<string name="places_sub">Säg ”navigate home” i valfri kartapp</string>
|
||||
<string name="places_intro">Säg ”navigate home”, ”take me to work” eller ”navigate to” med ett eget namn, så skickar Gearslip adressen till din kartapp. Röstkommandon fungerar bara på engelska än så länge.</string>
|
||||
<string name="places_home">Hem</string>
|
||||
<string name="places_work">Jobb</string>
|
||||
<string name="places_other">Plats %1$d</string>
|
||||
<string name="places_name">Namn</string>
|
||||
<string name="places_address">Adress</string>
|
||||
<string name="places_here">Använd min plats</string>
|
||||
<string name="places_finding">Söker…</string>
|
||||
<string name="places_not_found">Gearslip hittade inte var du är. Kontrollera att platstjänster är på.</string>
|
||||
<string name="places_clear">Rensa</string>
|
||||
</resources>
|
||||
@@ -817,4 +817,16 @@
|
||||
<string name="status_projecting_detail">Multimedya ünitesine aktarılıyor.</string>
|
||||
<string name="status_disconnected">Bağlantı kesildi</string>
|
||||
<string name="status_disconnected_detail">Multimedya ünitesi artık bağlı değil.</string>
|
||||
<string name="places_title">Kayıtlı yerler</string>
|
||||
<string name="places_sub">Herhangi bir harita uygulamasında “navigate home” deyin</string>
|
||||
<string name="places_intro">“navigate home”, “take me to work” ya da kendi verdiğiniz bir adla “navigate to” deyin, Gearslip adresi harita uygulamanıza göndersin. Sesli komutlar şimdilik yalnızca İngilizce.</string>
|
||||
<string name="places_home">Ev</string>
|
||||
<string name="places_work">İş</string>
|
||||
<string name="places_other">Yer %1$d</string>
|
||||
<string name="places_name">Ad</string>
|
||||
<string name="places_address">Adres</string>
|
||||
<string name="places_here">Bulunduğum yeri kullan</string>
|
||||
<string name="places_finding">Aranıyor…</string>
|
||||
<string name="places_not_found">Gearslip konumunuzu bulamadı. Konumun açık olduğundan emin olun.</string>
|
||||
<string name="places_clear">Temizle</string>
|
||||
</resources>
|
||||
@@ -833,4 +833,16 @@
|
||||
<string name="status_projecting_detail">Зображення передається на магнітолу.</string>
|
||||
<string name="status_disconnected">Від’єднано</string>
|
||||
<string name="status_disconnected_detail">Магнітола більше не підключена.</string>
|
||||
<string name="places_title">Збережені місця</string>
|
||||
<string name="places_sub">Скажіть «navigate home» у будь-якому застосунку карт</string>
|
||||
<string name="places_intro">Скажіть «navigate home», «take me to work» або «navigate to» зі своєю назвою, і Gearslip надішле адресу в застосунок карт. Поки голосові команди працюють лише англійською.</string>
|
||||
<string name="places_home">Дім</string>
|
||||
<string name="places_work">Робота</string>
|
||||
<string name="places_other">Місце %1$d</string>
|
||||
<string name="places_name">Назва</string>
|
||||
<string name="places_address">Адреса</string>
|
||||
<string name="places_here">Де я зараз</string>
|
||||
<string name="places_finding">Шукаю…</string>
|
||||
<string name="places_not_found">Gearslip не знайшов, де ви. Перевірте, чи увімкнено геолокацію.</string>
|
||||
<string name="places_clear">Очистити</string>
|
||||
</resources>
|
||||
@@ -809,4 +809,16 @@
|
||||
<string name="status_projecting_detail">正在向车机传输画面。</string>
|
||||
<string name="status_disconnected">已断开</string>
|
||||
<string name="status_disconnected_detail">车机已断开连接。</string>
|
||||
<string name="places_title">保存的地点</string>
|
||||
<string name="places_sub">在任何地图应用中说“navigate home”</string>
|
||||
<string name="places_intro">说“navigate home”“take me to work”,或“navigate to”加上你起的名字,Gearslip 就会把地址发给地图应用。语音命令目前只支持英语。</string>
|
||||
<string name="places_home">家</string>
|
||||
<string name="places_work">公司</string>
|
||||
<string name="places_other">地点 %1$d</string>
|
||||
<string name="places_name">名称</string>
|
||||
<string name="places_address">地址</string>
|
||||
<string name="places_here">使用当前位置</string>
|
||||
<string name="places_finding">正在定位…</string>
|
||||
<string name="places_not_found">Gearslip 找不到你的位置。请检查定位是否已开启。</string>
|
||||
<string name="places_clear">清除</string>
|
||||
</resources>
|
||||
@@ -861,4 +861,16 @@
|
||||
<string name="status_projecting_detail">Streaming to the head unit.</string>
|
||||
<string name="status_disconnected">Disconnected</string>
|
||||
<string name="status_disconnected_detail">The head unit is no longer connected.</string>
|
||||
<string name="places_title">Saved places</string>
|
||||
<string name="places_sub">Say “navigate home” in any map app</string>
|
||||
<string name="places_intro">Say “navigate home”, “take me to work” or “navigate to” a name of your own, and Gearslip sends that address to your map app.</string>
|
||||
<string name="places_home">Home</string>
|
||||
<string name="places_work">Work</string>
|
||||
<string name="places_other">Place %1$d</string>
|
||||
<string name="places_name">Name</string>
|
||||
<string name="places_address">Address</string>
|
||||
<string name="places_here">Use where I am</string>
|
||||
<string name="places_finding">Finding you…</string>
|
||||
<string name="places_not_found">Gearslip couldn’t find where you are. Check that location is on.</string>
|
||||
<string name="places_clear">Clear</string>
|
||||
</resources>
|
||||
Reference in new issue
Block a user