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