From 2f495596001b0660e3aee6b4ca4d389c53559b7e Mon Sep 17 00:00:00 2001 From: Seb3thehacker Date: Mon, 26 Oct 2026 23:57:39 -0700 Subject: [PATCH] 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. --- .../gearslip/car/AssistantCommands.kt | 22 +- .../seb3thehacker/gearslip/car/SavedPlaces.kt | 114 +++++++++ .../seb3thehacker/gearslip/ui/GearslipApp.kt | 11 +- .../gearslip/ui/HomeCarSettings.kt | 12 + .../gearslip/ui/SavedPlacesScreen.kt | 219 ++++++++++++++++++ gearslip/src/main/res/values-ar/strings.xml | 12 + gearslip/src/main/res/values-cs/strings.xml | 12 + gearslip/src/main/res/values-de/strings.xml | 12 + gearslip/src/main/res/values-es/strings.xml | 12 + gearslip/src/main/res/values-fr/strings.xml | 12 + gearslip/src/main/res/values-it/strings.xml | 12 + gearslip/src/main/res/values-ja/strings.xml | 12 + gearslip/src/main/res/values-ko/strings.xml | 12 + gearslip/src/main/res/values-nl/strings.xml | 12 + gearslip/src/main/res/values-pl/strings.xml | 12 + .../src/main/res/values-pt-rBR/strings.xml | 12 + gearslip/src/main/res/values-ru/strings.xml | 12 + gearslip/src/main/res/values-sv/strings.xml | 12 + gearslip/src/main/res/values-tr/strings.xml | 12 + gearslip/src/main/res/values-uk/strings.xml | 12 + .../src/main/res/values-zh-rCN/strings.xml | 12 + gearslip/src/main/res/values/strings.xml | 12 + 22 files changed, 579 insertions(+), 3 deletions(-) create mode 100644 gearslip/src/main/java/app/seb3thehacker/gearslip/car/SavedPlaces.kt create mode 100644 gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SavedPlacesScreen.kt diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/AssistantCommands.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/AssistantCommands.kt index 49d233f..e734a22 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/AssistantCommands.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/AssistantCommands.kt @@ -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}." } diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/SavedPlaces.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/SavedPlaces.kt new file mode 100644 index 0000000..e39e171 --- /dev/null +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/SavedPlaces.kt @@ -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 { + 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? { + 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 +} diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/GearslipApp.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/GearslipApp.kt index 7a56472..b1b9538 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/GearslipApp.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/GearslipApp.kt @@ -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 }, diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeCarSettings.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeCarSettings.kt index 03760ee..91d79b7 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeCarSettings.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HomeCarSettings.kt @@ -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) diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SavedPlacesScreen.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SavedPlacesScreen.kt new file mode 100644 index 0000000..b4a0e87 --- /dev/null +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SavedPlacesScreen.kt @@ -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() + } + } +} diff --git a/gearslip/src/main/res/values-ar/strings.xml b/gearslip/src/main/res/values-ar/strings.xml index 77d9cf9..eb3d5e0 100644 --- a/gearslip/src/main/res/values-ar/strings.xml +++ b/gearslip/src/main/res/values-ar/strings.xml @@ -849,4 +849,16 @@ البث إلى وحدة الرأس. انقطع الاتصال لم تعد وحدة الرأس متصلة. + الأماكن المحفوظة + قل “navigate home” في أي تطبيق خرائط + قل “navigate home” أو “take me to work” أو “navigate to” مع اسم اخترته، وسيرسل Gearslip العنوان إلى تطبيق الخرائط. الأوامر الصوتية بالإنجليزية فقط حاليًا. + المنزل + العمل + مكان %1$d + الاسم + العنوان + استخدام موقعي الحالي + جارٍ البحث… + لم يعثر Gearslip على موقعك. تأكد من تشغيل الموقع. + مسح diff --git a/gearslip/src/main/res/values-cs/strings.xml b/gearslip/src/main/res/values-cs/strings.xml index 3a64e67..92111b4 100644 --- a/gearslip/src/main/res/values-cs/strings.xml +++ b/gearslip/src/main/res/values-cs/strings.xml @@ -833,4 +833,16 @@ Streamování do autorádia. Odpojeno Autorádio už není připojené. + Uložená místa + Řekněte „navigate home“ v jakékoli mapové aplikaci + Ř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. + Domov + Práce + Místo %1$d + Název + Adresa + Použít mou polohu + Hledám… + Gearslip nenašel vaši polohu. Zkontrolujte, že je poloha zapnutá. + Vymazat diff --git a/gearslip/src/main/res/values-de/strings.xml b/gearslip/src/main/res/values-de/strings.xml index 8a0c68d..cdd8d36 100644 --- a/gearslip/src/main/res/values-de/strings.xml +++ b/gearslip/src/main/res/values-de/strings.xml @@ -695,4 +695,16 @@ Das Bild läuft auf dem Bildschirm des Autos. Getrennt Das Auto ist nicht mehr verbunden. + Gespeicherte Orte + Sag „navigate home“ in jeder Karten-App + 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. + Zuhause + Arbeit + Ort %1$d + Name + Adresse + Aktuellen Standort nehmen + Suche Standort… + Gearslip konnte deinen Standort nicht finden. Ist die Standortermittlung an? + Löschen diff --git a/gearslip/src/main/res/values-es/strings.xml b/gearslip/src/main/res/values-es/strings.xml index 3f520c9..d258fe3 100644 --- a/gearslip/src/main/res/values-es/strings.xml +++ b/gearslip/src/main/res/values-es/strings.xml @@ -695,4 +695,16 @@ Mostrando en la pantalla del coche. Desconectado El coche ya no está conectado. + Lugares guardados + Di “navigate home” en cualquier app de mapas + 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. + Casa + Trabajo + Lugar %1$d + Nombre + Dirección + Usar mi ubicación + Buscando… + Gearslip no encontró dónde estás. Comprueba que la ubicación esté activada. + Borrar diff --git a/gearslip/src/main/res/values-fr/strings.xml b/gearslip/src/main/res/values-fr/strings.xml index ad017da..8b34652 100644 --- a/gearslip/src/main/res/values-fr/strings.xml +++ b/gearslip/src/main/res/values-fr/strings.xml @@ -825,4 +825,16 @@ Diffusion vers l’autoradio. Déconnecté L’autoradio n’est plus connecté. + Lieux enregistrés + Dites « navigate home » dans n’importe quelle app de cartes + 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. + Domicile + Travail + Lieu %1$d + Nom + Adresse + Utiliser ma position + Recherche… + Gearslip n’a pas trouvé votre position. Vérifiez que la localisation est activée. + Effacer diff --git a/gearslip/src/main/res/values-it/strings.xml b/gearslip/src/main/res/values-it/strings.xml index 4fe7614..423d3db 100644 --- a/gearslip/src/main/res/values-it/strings.xml +++ b/gearslip/src/main/res/values-it/strings.xml @@ -825,4 +825,16 @@ Streaming verso l’autoradio. Disconnesso L’autoradio non è più connessa. + Luoghi salvati + Di’ “navigate home” in qualsiasi app di mappe + 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. + Casa + Lavoro + Luogo %1$d + Nome + Indirizzo + Usa la mia posizione + Ricerca… + Gearslip non ha trovato la tua posizione. Controlla che la localizzazione sia attiva. + Cancella diff --git a/gearslip/src/main/res/values-ja/strings.xml b/gearslip/src/main/res/values-ja/strings.xml index ecade71..5d97a11 100644 --- a/gearslip/src/main/res/values-ja/strings.xml +++ b/gearslip/src/main/res/values-ja/strings.xml @@ -809,4 +809,16 @@ ヘッドユニットに映像を送っています。 切断されました ヘッドユニットとの接続が切れました。 + 保存した場所 + どの地図アプリでも「navigate home」と話しかけられます + 「navigate home」「take me to work」、または「navigate to」と自分で付けた名前を言うと、Gearslip がその住所を地図アプリに送ります。音声コマンドは今のところ英語のみです。 + 自宅 + 職場 + 場所 %1$d + 名前 + 住所 + 現在地を使う + 検索中… + 現在地が見つかりませんでした。位置情報がオンになっているか確認してください。 + 消去 diff --git a/gearslip/src/main/res/values-ko/strings.xml b/gearslip/src/main/res/values-ko/strings.xml index a575b3c..eae3ffd 100644 --- a/gearslip/src/main/res/values-ko/strings.xml +++ b/gearslip/src/main/res/values-ko/strings.xml @@ -809,4 +809,16 @@ 헤드 유닛으로 스트리밍하고 있습니다. 연결 해제됨 헤드 유닛이 더 이상 연결되어 있지 않습니다. + 저장한 장소 + 어느 지도 앱에서든 “navigate home”이라고 말하세요 + “navigate home”, “take me to work” 또는 “navigate to”와 직접 붙인 이름을 말하면 Gearslip이 그 주소를 지도 앱으로 보냅니다. 음성 명령은 아직 영어만 됩니다. + 집 + 직장 + 장소 %1$d + 이름 + 주소 + 현재 위치 사용 + 찾는 중… + Gearslip이 현재 위치를 찾지 못했습니다. 위치가 켜져 있는지 확인하세요. + 지우기 diff --git a/gearslip/src/main/res/values-nl/strings.xml b/gearslip/src/main/res/values-nl/strings.xml index d162279..0edd823 100644 --- a/gearslip/src/main/res/values-nl/strings.xml +++ b/gearslip/src/main/res/values-nl/strings.xml @@ -817,4 +817,16 @@ Streamen naar de headunit. Verbinding verbroken De headunit is niet meer verbonden. + Opgeslagen plekken + Zeg “navigate home” in elke kaart-app + 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. + Thuis + Werk + Plek %1$d + Naam + Adres + Huidige locatie gebruiken + Zoeken… + Gearslip kon je locatie niet vinden. Staat locatie aan? + Wissen diff --git a/gearslip/src/main/res/values-pl/strings.xml b/gearslip/src/main/res/values-pl/strings.xml index f78fa68..c1cf27b 100644 --- a/gearslip/src/main/res/values-pl/strings.xml +++ b/gearslip/src/main/res/values-pl/strings.xml @@ -833,4 +833,16 @@ Przesyłanie obrazu do radia samochodowego. Rozłączono Radio samochodowe nie jest już połączone. + Zapisane miejsca + Powiedz „navigate home” w dowolnej aplikacji z mapami + 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. + Dom + Praca + Miejsce %1$d + Nazwa + Adres + Użyj mojej lokalizacji + Szukam… + Gearslip nie znalazł Twojej lokalizacji. Sprawdź, czy lokalizacja jest włączona. + Wyczyść diff --git a/gearslip/src/main/res/values-pt-rBR/strings.xml b/gearslip/src/main/res/values-pt-rBR/strings.xml index b69aa73..7481878 100644 --- a/gearslip/src/main/res/values-pt-rBR/strings.xml +++ b/gearslip/src/main/res/values-pt-rBR/strings.xml @@ -825,4 +825,16 @@ Transmitindo para a central multimídia. Desconectado A central multimídia não está mais conectada. + Lugares salvos + Diga “navigate home” em qualquer app de mapas + 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. + Casa + Trabalho + Lugar %1$d + Nome + Endereço + Usar onde estou + Procurando… + O Gearslip não encontrou onde você está. Veja se a localização está ligada. + Limpar diff --git a/gearslip/src/main/res/values-ru/strings.xml b/gearslip/src/main/res/values-ru/strings.xml index 675e921..7c4a9a6 100644 --- a/gearslip/src/main/res/values-ru/strings.xml +++ b/gearslip/src/main/res/values-ru/strings.xml @@ -833,4 +833,16 @@ Изображение передаётся на магнитолу. Отключено Магнитола больше не подключена. + Сохранённые места + Скажите «navigate home» в любом приложении карт + Скажите «navigate home», «take me to work» или «navigate to» со своим названием, и Gearslip отправит адрес в приложение карт. Пока голосовые команды работают только на английском. + Дом + Работа + Место %1$d + Название + Адрес + Где я сейчас + Ищу… + Gearslip не нашёл, где вы. Проверьте, что геолокация включена. + Очистить diff --git a/gearslip/src/main/res/values-sv/strings.xml b/gearslip/src/main/res/values-sv/strings.xml index e5621a1..d38335c 100644 --- a/gearslip/src/main/res/values-sv/strings.xml +++ b/gearslip/src/main/res/values-sv/strings.xml @@ -817,4 +817,16 @@ Strömmar till bilstereon. Frånkopplad Bilstereon är inte längre ansluten. + Sparade platser + Säg ”navigate home” i valfri kartapp + 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. + Hem + Jobb + Plats %1$d + Namn + Adress + Använd min plats + Söker… + Gearslip hittade inte var du är. Kontrollera att platstjänster är på. + Rensa diff --git a/gearslip/src/main/res/values-tr/strings.xml b/gearslip/src/main/res/values-tr/strings.xml index a770e4f..fbdf7e8 100644 --- a/gearslip/src/main/res/values-tr/strings.xml +++ b/gearslip/src/main/res/values-tr/strings.xml @@ -817,4 +817,16 @@ Multimedya ünitesine aktarılıyor. Bağlantı kesildi Multimedya ünitesi artık bağlı değil. + Kayıtlı yerler + Herhangi bir harita uygulamasında “navigate home” deyin + “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. + Ev + İş + Yer %1$d + Ad + Adres + Bulunduğum yeri kullan + Aranıyor… + Gearslip konumunuzu bulamadı. Konumun açık olduğundan emin olun. + Temizle diff --git a/gearslip/src/main/res/values-uk/strings.xml b/gearslip/src/main/res/values-uk/strings.xml index 0499585..58ce91c 100644 --- a/gearslip/src/main/res/values-uk/strings.xml +++ b/gearslip/src/main/res/values-uk/strings.xml @@ -833,4 +833,16 @@ Зображення передається на магнітолу. Від’єднано Магнітола більше не підключена. + Збережені місця + Скажіть «navigate home» у будь-якому застосунку карт + Скажіть «navigate home», «take me to work» або «navigate to» зі своєю назвою, і Gearslip надішле адресу в застосунок карт. Поки голосові команди працюють лише англійською. + Дім + Робота + Місце %1$d + Назва + Адреса + Де я зараз + Шукаю… + Gearslip не знайшов, де ви. Перевірте, чи увімкнено геолокацію. + Очистити diff --git a/gearslip/src/main/res/values-zh-rCN/strings.xml b/gearslip/src/main/res/values-zh-rCN/strings.xml index e2ffed7..dc0062d 100644 --- a/gearslip/src/main/res/values-zh-rCN/strings.xml +++ b/gearslip/src/main/res/values-zh-rCN/strings.xml @@ -809,4 +809,16 @@ 正在向车机传输画面。 已断开 车机已断开连接。 + 保存的地点 + 在任何地图应用中说“navigate home” + 说“navigate home”“take me to work”,或“navigate to”加上你起的名字,Gearslip 就会把地址发给地图应用。语音命令目前只支持英语。 + 家 + 公司 + 地点 %1$d + 名称 + 地址 + 使用当前位置 + 正在定位… + Gearslip 找不到你的位置。请检查定位是否已开启。 + 清除 diff --git a/gearslip/src/main/res/values/strings.xml b/gearslip/src/main/res/values/strings.xml index b55486a..8a384f9 100644 --- a/gearslip/src/main/res/values/strings.xml +++ b/gearslip/src/main/res/values/strings.xml @@ -861,4 +861,16 @@ Streaming to the head unit. Disconnected The head unit is no longer connected. + Saved places + Say “navigate home” in any map app + Say “navigate home”, “take me to work” or “navigate to” a name of your own, and Gearslip sends that address to your map app. + Home + Work + Place %1$d + Name + Address + Use where I am + Finding you… + Gearslip couldn’t find where you are. Check that location is on. + Clear