mirror of
https://github.com/twinksoftware/Gearslip.git
synced 2026-11-10 16:02:25 +01:00
car: the car screen's text and the spoken directions come from strings.xml, ready for translation; voice commands stay English
This commit is contained in:
1 parent
0fd7568536
commit
3fbabfa0ed
34 files changed
+900
-393
No files matched your search
@@ -1,5 +1,7 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import app.seb3thehacker.gearslip.car.theme.*
|
import app.seb3thehacker.gearslip.car.theme.*
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@@ -45,32 +47,28 @@ internal fun BluetoothPairOverlay() {
|
|||||||
val locationOff by LocationCheck.off.collectAsState()
|
val locationOff by LocationCheck.off.collectAsState()
|
||||||
val locationDismissed by LocationCheck.carDismissed.collectAsState()
|
val locationDismissed by LocationCheck.carDismissed.collectAsState()
|
||||||
if (missing && bluetoothAudio && warn && !dismissed) PairCard(
|
if (missing && bluetoothAudio && warn && !dismissed) PairCard(
|
||||||
title = "Bluetooth isn't connected",
|
title = stringResource(R.string.home_bt_title),
|
||||||
body = "Music plays over Bluetooth, but your phone isn't connected to the car. " +
|
body = stringResource(R.string.overlay_bt_body),
|
||||||
"Connect it from the car's Bluetooth menu or your phone's.",
|
secondary = stringResource(R.string.overlay_dont_show) to BluetoothAudioCheck::neverAgain,
|
||||||
secondary = "Don't show again" to BluetoothAudioCheck::neverAgain,
|
primary = stringResource(R.string.overlay_ok) to BluetoothAudioCheck::dismissOnCar,
|
||||||
primary = "OK" to BluetoothAudioCheck::dismissOnCar,
|
|
||||||
) else if (locationOff && !locationDismissed) PairCard(
|
) else if (locationOff && !locationDismissed) PairCard(
|
||||||
title = "Location is off",
|
title = stringResource(R.string.overlay_location_off),
|
||||||
body = "Map apps can't show where you are or give directions. Turn on location " +
|
body = stringResource(R.string.overlay_location_body),
|
||||||
"in your phone's quick settings when you're parked.",
|
secondary = stringResource(R.string.overlay_dont_show) to LocationCheck::neverAgain,
|
||||||
secondary = "Don't show again" to LocationCheck::neverAgain,
|
primary = stringResource(R.string.overlay_ok) to LocationCheck::dismissOnCar,
|
||||||
primary = "OK" to LocationCheck::dismissOnCar,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
CarBluetooth.Prompt.ASK -> PairCard(
|
CarBluetooth.Prompt.ASK -> PairCard(
|
||||||
title = "Pair with this car?",
|
title = stringResource(R.string.overlay_pair_title),
|
||||||
body = (if (bluetoothAudio) "Calls and music play" else "Calls play") +
|
body = stringResource(if (bluetoothAudio) R.string.overlay_pair_body_music else R.string.overlay_pair_body),
|
||||||
" through the car's speakers once your phone and the car are paired. Your phone " +
|
secondary = stringResource(R.string.worked_not_now) to CarBluetooth::notNow,
|
||||||
"then asks you to confirm a code.",
|
primary = stringResource(R.string.overlay_pair) to CarBluetooth::pair,
|
||||||
secondary = "Not now" to CarBluetooth::notNow,
|
|
||||||
primary = "Pair" to CarBluetooth::pair,
|
|
||||||
)
|
)
|
||||||
CarBluetooth.Prompt.PAIRING -> PairCard(
|
CarBluetooth.Prompt.PAIRING -> PairCard(
|
||||||
title = "Confirm on your phone",
|
title = stringResource(R.string.overlay_confirm_title),
|
||||||
body = "Check the code on your phone and tap Pair. The car may ask too.",
|
body = stringResource(R.string.overlay_confirm_body),
|
||||||
secondary = null,
|
secondary = null,
|
||||||
primary = "Hide" to CarBluetooth::hide,
|
primary = stringResource(R.string.carset_hide) to CarBluetooth::hide,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import app.seb3thehacker.gearslip.car.theme.*
|
import app.seb3thehacker.gearslip.car.theme.*
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -63,7 +65,7 @@ private fun IncomingCall(s: CallUiState.Ringing, onAnswer: () -> Unit, onDecline
|
|||||||
)
|
)
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
Text(
|
Text(
|
||||||
"Incoming call",
|
stringResource(R.string.call_incoming),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
@@ -76,13 +78,13 @@ private fun IncomingCall(s: CallUiState.Ringing, onAnswer: () -> Unit, onDecline
|
|||||||
)
|
)
|
||||||
Spacer(Modifier.height(56.dp))
|
Spacer(Modifier.height(56.dp))
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(48.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(48.dp)) {
|
||||||
CallButton(Icons.Filled.Close, "Decline", MaterialTheme.colorScheme.error, enabled = s.canDecline, onClick = onDecline)
|
CallButton(Icons.Filled.Close, stringResource(R.string.call_decline), MaterialTheme.colorScheme.error, enabled = s.canDecline, onClick = onDecline)
|
||||||
CallButton(Icons.Filled.Call, "Answer", MaterialTheme.colorScheme.primary, enabled = true, onClick = onAnswer)
|
CallButton(Icons.Filled.Call, stringResource(R.string.call_answer), MaterialTheme.colorScheme.primary, enabled = true, onClick = onAnswer)
|
||||||
}
|
}
|
||||||
if (!s.canDecline) {
|
if (!s.canDecline) {
|
||||||
Spacer(Modifier.height(20.dp))
|
Spacer(Modifier.height(20.dp))
|
||||||
Text(
|
Text(
|
||||||
"Already ringing out loud - answer here, or let it ring.",
|
stringResource(R.string.call_ringing),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import app.seb3thehacker.gearslip.car.theme.*
|
import app.seb3thehacker.gearslip.car.theme.*
|
||||||
import android.view.SurfaceHolder
|
import android.view.SurfaceHolder
|
||||||
import android.view.SurfaceView
|
import android.view.SurfaceView
|
||||||
@@ -249,14 +251,14 @@ private fun ChromeBar(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
status.app ?: "Car app",
|
status.app ?: stringResource(R.string.stage_car_app),
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
GsIconButton(
|
GsIconButton(
|
||||||
androidx.compose.material.icons.Icons.Filled.Close,
|
androidx.compose.material.icons.Icons.Filled.Close,
|
||||||
"Close ${status.app ?: "the app"}",
|
status.app?.let { stringResource(R.string.stage_close, it) } ?: stringResource(R.string.stage_close_app),
|
||||||
onDisconnect,
|
onDisconnect,
|
||||||
size = 36.dp,
|
size = 36.dp,
|
||||||
iconSize = 20.dp,
|
iconSize = 20.dp,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Call
|
import androidx.compose.material.icons.filled.Call
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
@@ -13,7 +15,9 @@ import app.seb3thehacker.gearslip.ProjectedContent
|
|||||||
/** One tile in the launcher and the screen it opens. */
|
/** One tile in the launcher and the screen it opens. */
|
||||||
class CarApp(
|
class CarApp(
|
||||||
val id: String,
|
val id: String,
|
||||||
|
/** English, for matching what a driver says; [labelRes] is what the screen shows. */
|
||||||
val label: String,
|
val label: String,
|
||||||
|
@param:StringRes val labelRes: Int,
|
||||||
val icon: ImageVector,
|
val icon: ImageVector,
|
||||||
val content: @Composable () -> Unit,
|
val content: @Composable () -> Unit,
|
||||||
)
|
)
|
||||||
@@ -22,16 +26,16 @@ class CarApp(
|
|||||||
object CarApps {
|
object CarApps {
|
||||||
|
|
||||||
val all: List<CarApp> = listOf(
|
val all: List<CarApp> = listOf(
|
||||||
CarApp("web", "Browser", Icons.Filled.Search) {
|
CarApp("web", "Browser", R.string.app_browser, Icons.Filled.Search) {
|
||||||
WebApp(content = ProjectedContent.resolve(LocalContext.current))
|
WebApp(content = ProjectedContent.resolve(LocalContext.current))
|
||||||
},
|
},
|
||||||
CarApp("phone", "Screen sharing", Icons.Filled.Share) {
|
CarApp("phone", "Screen sharing", R.string.app_screen_sharing, Icons.Filled.Share) {
|
||||||
PhoneAppsScreen()
|
PhoneAppsScreen()
|
||||||
},
|
},
|
||||||
CarApp("dialer", "Phone", Icons.Filled.Call) {
|
CarApp("dialer", "Phone", R.string.carset_phone, Icons.Filled.Call) {
|
||||||
PhoneDialerScreen()
|
PhoneDialerScreen()
|
||||||
},
|
},
|
||||||
CarApp("settings", "Settings", Icons.Filled.Settings) {
|
CarApp("settings", "Settings", R.string.settings_title, Icons.Filled.Settings) {
|
||||||
CarSettingsScreen()
|
CarSettingsScreen()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.bluetooth.BluetoothAdapter
|
import android.bluetooth.BluetoothAdapter
|
||||||
@@ -111,7 +113,7 @@ object CarBluetooth {
|
|||||||
// If the car never answers on its channel, pair from the phone anyway.
|
// If the car never answers on its channel, pair from the phone anyway.
|
||||||
main.postDelayed({ startBond("the car didn't answer the pairing request") }, CAR_REPLY_WAIT_MS)
|
main.postDelayed({ startBond("the car didn't answer the pairing request") }, CAR_REPLY_WAIT_MS)
|
||||||
main.postDelayed({
|
main.postDelayed({
|
||||||
if (pairing) finish("Pairing didn't finish. Pair from the car's Bluetooth menu instead.")
|
if (pairing) finish(R.string.bt_pair_unfinished)
|
||||||
}, GIVE_UP_MS)
|
}, GIVE_UP_MS)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +170,7 @@ object CarBluetooth {
|
|||||||
bondStarted = true
|
bondStarted = true
|
||||||
val started = runCatching { device.createBond() }.getOrDefault(false)
|
val started = runCatching { device.createBond() }.getOrDefault(false)
|
||||||
log.i("pairing from the phone ($why): ${if (started) "started" else "refused"}")
|
log.i("pairing from the phone ($why): ${if (started) "started" else "refused"}")
|
||||||
if (!started) finish("Pairing didn't start. Pair from the car's Bluetooth menu instead.")
|
if (!started) finish(R.string.bt_pair_not_started)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun watchBonding(device: BluetoothDevice) {
|
private fun watchBonding(device: BluetoothDevice) {
|
||||||
@@ -184,8 +186,8 @@ object CarBluetooth {
|
|||||||
log.i("bond state $previous -> $state")
|
log.i("bond state $previous -> $state")
|
||||||
when {
|
when {
|
||||||
state == BluetoothDevice.BOND_BONDING -> bondStarted = true
|
state == BluetoothDevice.BOND_BONDING -> bondStarted = true
|
||||||
state == BluetoothDevice.BOND_BONDED -> finish("Paired. Calls now play through the car.")
|
state == BluetoothDevice.BOND_BONDED -> finish(R.string.bt_paired)
|
||||||
previous == BluetoothDevice.BOND_BONDING -> finish("Pairing didn't finish. Pair from the car's Bluetooth menu instead.")
|
previous == BluetoothDevice.BOND_BONDING -> finish(R.string.bt_pair_unfinished)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,12 +195,12 @@ object CarBluetooth {
|
|||||||
receiver = r
|
receiver = r
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun finish(message: String) {
|
private fun finish(@StringRes message: Int) {
|
||||||
pairing = false
|
pairing = false
|
||||||
stopWatching()
|
stopWatching()
|
||||||
main.removeCallbacksAndMessages(null)
|
main.removeCallbacksAndMessages(null)
|
||||||
_prompt.value = Prompt.NONE
|
_prompt.value = Prompt.NONE
|
||||||
CarToasts.show(this, message, CarToast.LENGTH_LONG)
|
CarToasts.show(this, appContext?.getString(message) ?: return, CarToast.LENGTH_LONG)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopWatching() {
|
private fun stopWatching() {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
@@ -60,7 +62,7 @@ fun CarDashboardScreen() {
|
|||||||
.padding(horizontal = 24.dp, vertical = 16.dp),
|
.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||||
) {
|
) {
|
||||||
ScreenHeader("Today")
|
ScreenHeader(stringResource(R.string.dash_today))
|
||||||
WeatherCard(context)
|
WeatherCard(context)
|
||||||
AgendaCard(context)
|
AgendaCard(context)
|
||||||
NotificationsSection()
|
NotificationsSection()
|
||||||
@@ -84,9 +86,9 @@ private fun WeatherCard(context: Context) {
|
|||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
when (val s = state) {
|
when (val s = state) {
|
||||||
WeatherState.Loading -> Text("Getting weather…", style = MaterialTheme.typography.bodyLarge)
|
WeatherState.Loading -> Text(stringResource(R.string.dash_getting_weather), style = MaterialTheme.typography.bodyLarge)
|
||||||
is WeatherState.Unavailable -> Text(
|
is WeatherState.Unavailable -> Text(
|
||||||
s.reason, style = MaterialTheme.typography.bodyLarge,
|
stringResource(s.reason), style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
is WeatherState.Ready -> {
|
is WeatherState.Ready -> {
|
||||||
@@ -94,7 +96,7 @@ private fun WeatherCard(context: Context) {
|
|||||||
WeatherIcon(w.code, w.isDay, size = 44.dp)
|
WeatherIcon(w.code, w.isDay, size = 44.dp)
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
"${w.temp}${w.tempUnit} · ${Weather.describe(w.code)}",
|
"${w.temp}${w.tempUnit} · ${Weather.condition(w.code)}",
|
||||||
style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold,
|
style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold,
|
||||||
)
|
)
|
||||||
Text(w.place, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Text(w.place, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
@@ -112,12 +114,12 @@ private fun AgendaCard(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
Text("Coming up", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary)
|
Text(stringResource(R.string.dash_coming_up), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary)
|
||||||
val list = events
|
val list = events
|
||||||
when {
|
when {
|
||||||
list == null -> Text("Loading calendar…", style = MaterialTheme.typography.bodyMedium)
|
list == null -> Text(stringResource(R.string.dash_loading_calendar), style = MaterialTheme.typography.bodyMedium)
|
||||||
list.isEmpty() -> Text(
|
list.isEmpty() -> Text(
|
||||||
"Nothing in the next week.",
|
stringResource(R.string.dash_nothing_week),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
@@ -135,7 +137,7 @@ private fun EventRow(event: AgendaEvent) {
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
eventWhen(event),
|
eventWhen(event, stringResource(R.string.dash_all_day)),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
)
|
)
|
||||||
@@ -149,14 +151,14 @@ private fun EventRow(event: AgendaEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** "Today" events show just a time; later days get a weekday so a week's list stays readable. */
|
/** stringResource(R.string.dash_today) events show just a time; later days get a weekday so a week's list stays readable. */
|
||||||
private fun eventWhen(event: AgendaEvent): String {
|
private fun eventWhen(event: AgendaEvent, allDay: String): String {
|
||||||
val zone = if (event.allDay) TimeZone.getTimeZone("UTC") else TimeZone.getDefault()
|
val zone = if (event.allDay) TimeZone.getTimeZone("UTC") else TimeZone.getDefault()
|
||||||
val start = Calendar.getInstance(zone).apply { timeInMillis = event.startMillis }
|
val start = Calendar.getInstance(zone).apply { timeInMillis = event.startMillis }
|
||||||
val today = Calendar.getInstance()
|
val today = Calendar.getInstance()
|
||||||
val sameDay = start.get(Calendar.YEAR) == today.get(Calendar.YEAR) &&
|
val sameDay = start.get(Calendar.YEAR) == today.get(Calendar.YEAR) &&
|
||||||
start.get(Calendar.DAY_OF_YEAR) == today.get(Calendar.DAY_OF_YEAR)
|
start.get(Calendar.DAY_OF_YEAR) == today.get(Calendar.DAY_OF_YEAR)
|
||||||
val time = if (event.allDay) "All day" else DateFormat.getTimeInstance(DateFormat.SHORT).format(Date(event.startMillis))
|
val time = if (event.allDay) allDay else DateFormat.getTimeInstance(DateFormat.SHORT).format(Date(event.startMillis))
|
||||||
if (sameDay || (event.allDay && start.before(today))) return time
|
if (sameDay || (event.allDay && start.before(today))) return time
|
||||||
return SimpleDateFormat("EEE", Locale.getDefault()).apply { timeZone = zone }.format(Date(event.startMillis)) + " " + time
|
return SimpleDateFormat("EEE", Locale.getDefault()).apply { timeZone = zone }.format(Date(event.startMillis)) + " " + time
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import app.seb3thehacker.gearslip.car.theme.*
|
import app.seb3thehacker.gearslip.car.theme.*
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -169,19 +170,19 @@ private fun MapPane(status: CarAppConnection.Status, frame: CarEnvironment.Frame
|
|||||||
val scope = androidx.compose.runtime.rememberCoroutineScope()
|
val scope = androidx.compose.runtime.rememberCoroutineScope()
|
||||||
when (status.phase) {
|
when (status.phase) {
|
||||||
CarAppConnection.Phase.BINDING, CarAppConnection.Phase.HANDSHAKE ->
|
CarAppConnection.Phase.BINDING, CarAppConnection.Phase.HANDSHAKE ->
|
||||||
Text("Starting ${status.app ?: "the map"}…", style = MaterialTheme.typography.titleMedium)
|
Text(status.app?.let { stringResource(R.string.home_starting, it) } ?: stringResource(R.string.home_starting_map), style = MaterialTheme.typography.titleMedium)
|
||||||
CarAppConnection.Phase.REJECTED, CarAppConnection.Phase.FAILED -> {
|
CarAppConnection.Phase.REJECTED, CarAppConnection.Phase.FAILED -> {
|
||||||
Text("${status.app ?: "The map app"} would not start.", style = MaterialTheme.typography.titleMedium)
|
Text(status.app?.let { stringResource(R.string.home_failed, it) } ?: stringResource(R.string.home_failed_map), style = MaterialTheme.typography.titleMedium)
|
||||||
status.detail?.let { Text(it, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) }
|
status.detail?.let { Text(it, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) }
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
StatusButton("Choose another app", GsTone.Primary, navigator::apps)
|
StatusButton(stringResource(R.string.home_choose_app), GsTone.Primary, navigator::apps)
|
||||||
}
|
}
|
||||||
else -> if (!hasNavApps) {
|
else -> if (!hasNavApps) {
|
||||||
// Nothing to auto-select and nothing to pick from Car apps either - this is the
|
// Nothing to auto-select and nothing to pick from Car apps either - this is the
|
||||||
// only case worth a distinct screen, since every other IDLE case resolves itself.
|
// only case worth a distinct screen, since every other IDLE case resolves itself.
|
||||||
Text("No mapping apps found.", style = MaterialTheme.typography.titleMedium)
|
Text(stringResource(R.string.home_no_maps), style = MaterialTheme.typography.titleMedium)
|
||||||
Text(
|
Text(
|
||||||
"Install a navigation app that supports Android Auto and it will start here on its own.",
|
stringResource(R.string.home_no_maps_body),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
@@ -189,17 +190,17 @@ private fun MapPane(status: CarAppConnection.Status, frame: CarEnvironment.Frame
|
|||||||
// IDLE with a last app on record means the driver stopped it themselves - it will
|
// IDLE with a last app on record means the driver stopped it themselves - it will
|
||||||
// not come back on its own, so the screen must offer a way to bring it back rather
|
// not come back on its own, so the screen must offer a way to bring it back rather
|
||||||
// than sit on a "starting" message that never resolves.
|
// than sit on a "starting" message that never resolves.
|
||||||
Text("Map stopped", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
Text(stringResource(R.string.home_map_stopped), style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||||
Text(
|
Text(
|
||||||
"It stays closed until you open it again.",
|
stringResource(R.string.home_map_stopped_body),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
// Side by side, the same size, the likely one lit: one clear choice, not a stack.
|
// Side by side, the same size, the likely one lit: one clear choice, not a stack.
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(14.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||||
StatusButton("Reopen the map", GsTone.Primary) { scope.launch { CarServices.reconnectNav(frame) } }
|
StatusButton(stringResource(R.string.home_reopen_map), GsTone.Primary) { scope.launch { CarServices.reconnectNav(frame) } }
|
||||||
StatusButton("Car apps", GsTone.Tonal) { navigator.apps() }
|
StatusButton(stringResource(R.string.home_car_apps), GsTone.Tonal) { navigator.apps() }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Autostart is still picking one - a navigation app is installed, so this resolves
|
// Autostart is still picking one - a navigation app is installed, so this resolves
|
||||||
@@ -274,13 +275,13 @@ private fun DockedPlayer(now: NowPlaying, modifier: Modifier) {
|
|||||||
Spacer(Modifier.height(10.dp))
|
Spacer(Modifier.height(10.dp))
|
||||||
PlayModeRow(now, media, size = 50.dp, modifier = Modifier.fillMaxWidth()) {
|
PlayModeRow(now, media, size = 50.dp, modifier = Modifier.fillMaxWidth()) {
|
||||||
GsIconButton(
|
GsIconButton(
|
||||||
ImageVector.vectorResource(R.drawable.lyrics_24), "Lyrics", navigator::showSideLyrics,
|
ImageVector.vectorResource(R.drawable.lyrics_24), stringResource(R.string.storage_lyrics), navigator::showSideLyrics,
|
||||||
size = 50.dp, iconSize = 26.dp,
|
size = 50.dp, iconSize = 26.dp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
GsIconButton(
|
GsIconButton(
|
||||||
Icons.Filled.Close, "Back to the nav bar", navigator::closeSidePanel,
|
Icons.Filled.Close, stringResource(R.string.home_back_nav), navigator::closeSidePanel,
|
||||||
Modifier.align(Alignment.TopEnd), size = 36.dp, iconSize = 20.dp,
|
Modifier.align(Alignment.TopEnd), size = 36.dp, iconSize = 20.dp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -313,9 +314,9 @@ private fun SideLyrics(now: NowPlaying, modifier: Modifier) {
|
|||||||
maxLines = 1, overflow = TextOverflow.Ellipsis,
|
maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
// The way back to the player, as the player's own Lyrics button is the way here.
|
// The way back to the player, as the player's own Lyrics button is the way here.
|
||||||
GsIconButton(MediaIcons.MusicNote, "Show the player", navigator::showSidePlayer, size = 36.dp, iconSize = 20.dp)
|
GsIconButton(MediaIcons.MusicNote, stringResource(R.string.home_show_player), navigator::showSidePlayer, size = 36.dp, iconSize = 20.dp)
|
||||||
Spacer(Modifier.size(6.dp))
|
Spacer(Modifier.size(6.dp))
|
||||||
GsIconButton(Icons.Filled.Close, "Hide lyrics", navigator::closeSidePanel, size = 36.dp, iconSize = 20.dp)
|
GsIconButton(Icons.Filled.Close, stringResource(R.string.home_hide_lyrics), navigator::closeSidePanel, size = 36.dp, iconSize = 20.dp)
|
||||||
}
|
}
|
||||||
LyricsPanel(now, Modifier.weight(1f).fillMaxWidth(), lineStyle = MaterialTheme.typography.headlineSmall)
|
LyricsPanel(now, Modifier.weight(1f).fillMaxWidth(), lineStyle = MaterialTheme.typography.headlineSmall)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import app.seb3thehacker.gearslip.car.theme.*
|
import app.seb3thehacker.gearslip.car.theme.*
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -153,7 +155,7 @@ fun CarKeyboard(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
Key(if (symbols) "abc" else "?123", Modifier.weight(1.4f)) { symbols = !symbols }
|
Key(if (symbols) "abc" else "?123", Modifier.weight(1.4f)) { symbols = !symbols }
|
||||||
Key("space", Modifier.weight(if (onDismiss != null) 3.2f else 4f)) { onTextChange("$text ") }
|
Key(stringResource(R.string.keyboard_space), Modifier.weight(if (onDismiss != null) 3.2f else 4f)) { onTextChange("$text ") }
|
||||||
if (onDismiss != null) {
|
if (onDismiss != null) {
|
||||||
DismissKey(onDismiss, Modifier.weight(1.4f))
|
DismissKey(onDismiss, Modifier.weight(1.4f))
|
||||||
}
|
}
|
||||||
@@ -200,7 +202,7 @@ private fun DismissKey(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
|||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Filled.Close, contentDescription = "Hide keyboard")
|
Icon(Icons.Filled.Close, contentDescription = stringResource(R.string.keyboard_hide))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,7 +222,7 @@ private fun SubmitKey(modifier: Modifier, search: Boolean, image: ImageVector?,
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
image ?: if (search) Icons.Filled.Search else Icons.Filled.ArrowBack,
|
image ?: if (search) Icons.Filled.Search else Icons.Filled.ArrowBack,
|
||||||
contentDescription = "Submit",
|
contentDescription = stringResource(R.string.keyboard_submit),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import app.seb3thehacker.gearslip.car.theme.*
|
import app.seb3thehacker.gearslip.car.theme.*
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
@@ -127,7 +129,7 @@ internal object LauncherCache {
|
|||||||
// notifications - rather than a filter per kind that has to know about the others.
|
// notifications - rather than a filter per kind that has to know about the others.
|
||||||
val tiles = (carApps.keys + mediaApps.keys + messagingApps.keys).flatMap { pkg ->
|
val tiles = (carApps.keys + mediaApps.keys + messagingApps.keys).flatMap { pkg ->
|
||||||
val icon by lazy { iconOf(pkg) }
|
val icon by lazy { iconOf(pkg) }
|
||||||
tilesFor(carApps[pkg], mediaApps[pkg], messagingApps[pkg]).map { tile ->
|
tilesFor(carApps[pkg], mediaApps[pkg], messagingApps[pkg], context.getString(R.string.media_browse)).map { tile ->
|
||||||
when (tile) {
|
when (tile) {
|
||||||
is AppTile.Screen -> Entry(tile.label, icon, template = tile.app)
|
is AppTile.Screen -> Entry(tile.label, icon, template = tile.app)
|
||||||
is AppTile.Player -> Entry(tile.app.label, icon, media = tile.app)
|
is AppTile.Player -> Entry(tile.app.label, icon, media = tile.app)
|
||||||
@@ -163,6 +165,7 @@ private fun tilesFor(
|
|||||||
car: TemplateApp?,
|
car: TemplateApp?,
|
||||||
media: app.seb3thehacker.gearslip.media.MediaApp?,
|
media: app.seb3thehacker.gearslip.media.MediaApp?,
|
||||||
messaging: app.seb3thehacker.gearslip.notify.MessagingApp?,
|
messaging: app.seb3thehacker.gearslip.notify.MessagingApp?,
|
||||||
|
browse: String,
|
||||||
): List<AppTile> {
|
): List<AppTile> {
|
||||||
if (car != null && !car.isMediaScreen && car.kind != "messaging") return listOf(AppTile.Screen(car, car.label))
|
if (car != null && !car.isMediaScreen && car.kind != "messaging") return listOf(AppTile.Screen(car, car.label))
|
||||||
val pkg = car?.component?.packageName ?: media?.component?.packageName ?: messaging?.packageName ?: return emptyList()
|
val pkg = car?.component?.packageName ?: media?.component?.packageName ?: messaging?.packageName ?: return emptyList()
|
||||||
@@ -170,7 +173,7 @@ private fun tilesFor(
|
|||||||
val screenRefused = KnownApps.screenBroken(pkg) && KnownApps.playerWorks(pkg)
|
val screenRefused = KnownApps.screenBroken(pkg) && KnownApps.playerWorks(pkg)
|
||||||
val messengerScreen = messaging != null && (car?.kind == "messaging" || car?.kind == "app")
|
val messengerScreen = messaging != null && (car?.kind == "messaging" || car?.kind == "app")
|
||||||
if (car != null && !messengerScreen && !(media != null && screenRefused)) {
|
if (car != null && !messengerScreen && !(media != null && screenRefused)) {
|
||||||
add(AppTile.Screen(car, if (media != null) "${car.label} · Browse" else car.label))
|
add(AppTile.Screen(car, if (media != null) "${car.label} · $browse" else car.label))
|
||||||
}
|
}
|
||||||
if (media != null) add(AppTile.Player(media))
|
if (media != null) add(AppTile.Player(media))
|
||||||
else if (messaging != null) add(AppTile.Messages(messaging))
|
else if (messaging != null) add(AppTile.Messages(messaging))
|
||||||
@@ -280,7 +283,7 @@ fun CarLauncher() {
|
|||||||
val pkg = entry.packageName?.takeIf { entry.messaging == null }
|
val pkg = entry.packageName?.takeIf { entry.messaging == null }
|
||||||
val id = entry.componentId
|
val id = entry.componentId
|
||||||
return Tile(
|
return Tile(
|
||||||
entry.label, entry.icon, entry.builtIn?.glyph,
|
entry.builtIn?.let { context.getString(it.labelRes) } ?: entry.label, entry.icon, entry.builtIn?.glyph,
|
||||||
broken = pkg != null && (KnownApps.isBroken(context, pkg) || (entry.template != null && KnownApps.screenBroken(pkg))),
|
broken = pkg != null && (KnownApps.isBroken(context, pkg) || (entry.template != null && KnownApps.screenBroken(pkg))),
|
||||||
onLongClick = id?.let { { navigator.showAppMenu(it) } },
|
onLongClick = id?.let { { navigator.showAppMenu(it) } },
|
||||||
) { openApp(entry, id?.let { running[it] }, navigator, frame) }
|
) { openApp(entry, id?.let { running[it] }, navigator, frame) }
|
||||||
@@ -307,7 +310,7 @@ private fun BrokenBadge(size: androidx.compose.ui.unit.Dp, modifier: Modifier =
|
|||||||
modifier.size(size).background(Color(0xFFD32F2F), CircleShape),
|
modifier.size(size).background(Color(0xFFD32F2F), CircleShape),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Filled.Close, contentDescription = "Doesn't work with Gearslip", tint = Color.White, modifier = Modifier.size(size * 0.66f))
|
Icon(Icons.Filled.Close, contentDescription = stringResource(R.string.launcher_broken), tint = Color.White, modifier = Modifier.size(size * 0.66f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.material.icons.filled.Call
|
import androidx.compose.material.icons.filled.Call
|
||||||
import app.seb3thehacker.gearslip.call.CarCalls
|
import app.seb3thehacker.gearslip.call.CarCalls
|
||||||
import app.seb3thehacker.gearslip.call.CallUiState
|
import app.seb3thehacker.gearslip.call.CallUiState
|
||||||
@@ -116,12 +117,12 @@ private fun BreadcrumbBar(navigator: CarNavigator) {
|
|||||||
Modifier.fillMaxWidth().height(40.dp).padding(start = 2.dp, end = 10.dp),
|
Modifier.fillMaxWidth().height(40.dp).padding(start = 2.dp, end = 10.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
GsIconButton(Icons.AutoMirrored.Filled.ArrowBack, "Back", navigator::back, size = 32.dp, iconSize = 20.dp)
|
GsIconButton(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back), navigator::back, size = 32.dp, iconSize = 20.dp)
|
||||||
Spacer(Modifier.width(2.dp))
|
Spacer(Modifier.width(2.dp))
|
||||||
trail.forEachIndexed { index, crumb ->
|
trail.forEachIndexed { index, crumb ->
|
||||||
val isLast = index == trail.lastIndex
|
val isLast = index == trail.lastIndex
|
||||||
Text(
|
Text(
|
||||||
crumb.label,
|
crumb.shown(),
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
fontWeight = if (isLast) FontWeight.SemiBold else FontWeight.Normal,
|
fontWeight = if (isLast) FontWeight.SemiBold else FontWeight.Normal,
|
||||||
color = if (isLast) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant,
|
color = if (isLast) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
@@ -283,7 +284,7 @@ private fun NavHomeButton(navigator: CarNavigator, entries: List<Entry>, running
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Icon(
|
Icon(
|
||||||
ImageVector.vectorResource(R.drawable.navigation_24), contentDescription = "Home",
|
ImageVector.vectorResource(R.drawable.navigation_24), contentDescription = stringResource(R.string.crumb_home),
|
||||||
modifier = Modifier.padding(NAV_ITEM_PADDING).size(28.dp),
|
modifier = Modifier.padding(NAV_ITEM_PADDING).size(28.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -344,7 +345,7 @@ private fun NavAppIcon(
|
|||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
) {
|
) {
|
||||||
entry.icon?.let { Image(it, entry.label, Modifier.size(30.dp).clip(RoundedCornerShape(8.dp))) }
|
entry.icon?.let { Image(it, entry.label, Modifier.size(30.dp).clip(RoundedCornerShape(8.dp))) }
|
||||||
entry.builtIn?.let { Icon(it.glyph, entry.label, Modifier.size(26.dp)) }
|
entry.builtIn?.let { Icon(it.glyph, entry.shownLabel(), Modifier.size(26.dp)) }
|
||||||
// Only running apps get the line along the bottom edge, the usual taskbar mark for it.
|
// Only running apps get the line along the bottom edge, the usual taskbar mark for it.
|
||||||
if (open) {
|
if (open) {
|
||||||
Box(
|
Box(
|
||||||
@@ -495,7 +496,7 @@ private fun NavCallPill(call: CallUiState.Active, modifier: Modifier = Modifier)
|
|||||||
) {
|
) {
|
||||||
Icon(Icons.Filled.Call, contentDescription = null, modifier = Modifier.size(20.dp))
|
Icon(Icons.Filled.Call, contentDescription = null, modifier = Modifier.size(20.dp))
|
||||||
Text(
|
Text(
|
||||||
call.caller?.label ?: "On a call",
|
call.caller?.label ?: stringResource(R.string.navbar_on_call),
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
@@ -510,12 +511,12 @@ private fun NavCallPill(call: CallUiState.Active, modifier: Modifier = Modifier)
|
|||||||
Spacer(Modifier.width(2.dp))
|
Spacer(Modifier.width(2.dp))
|
||||||
GsIconButton(
|
GsIconButton(
|
||||||
if (muted) MediaIcons.MicOff else MediaIcons.Mic,
|
if (muted) MediaIcons.MicOff else MediaIcons.Mic,
|
||||||
if (muted) "Unmute" else "Mute",
|
stringResource(if (muted) R.string.navbar_unmute else R.string.navbar_mute),
|
||||||
CarCalls::toggleMute,
|
CarCalls::toggleMute,
|
||||||
tone = if (muted) GsTone.Primary else GsTone.Neutral, size = 36.dp, iconSize = 22.dp,
|
tone = if (muted) GsTone.Primary else GsTone.Neutral, size = 36.dp, iconSize = 22.dp,
|
||||||
)
|
)
|
||||||
GsIconButton(
|
GsIconButton(
|
||||||
MediaIcons.CallEnd, "End call", { CarCalls.end(context) },
|
MediaIcons.CallEnd, stringResource(R.string.navbar_end_call), { CarCalls.end(context) },
|
||||||
colors = GsColors(scheme.error, scheme.onError), size = 36.dp, iconSize = 22.dp,
|
colors = GsColors(scheme.error, scheme.onError), size = 36.dp, iconSize = 22.dp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -523,9 +524,10 @@ private fun NavCallPill(call: CallUiState.Active, modifier: Modifier = Modifier)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** "Title - Artist" on one line. The text itself cuts it off, and only where the pill runs out of room. */
|
/** "Title - Artist" on one line. The text itself cuts it off, and only where the pill runs out of room. */
|
||||||
|
@Composable
|
||||||
private fun nowPlayingLabel(title: String, artist: String): String =
|
private fun nowPlayingLabel(title: String, artist: String): String =
|
||||||
listOf(title.trim(), artist.trim()).filter { it.isNotEmpty() }.joinToString(" - ")
|
listOf(title.trim(), artist.trim()).filter { it.isNotEmpty() }.joinToString(" - ")
|
||||||
.ifEmpty { "Nothing playing" }
|
.ifEmpty { stringResource(R.string.media_nothing_playing) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The player in the nav bar, on every screen. Art and title open the full media screen;
|
* The player in the nav bar, on every screen. Art and title open the full media screen;
|
||||||
@@ -607,19 +609,19 @@ private fun NavNowPlaying(navigator: CarNavigator, selected: Boolean, modifier:
|
|||||||
}
|
}
|
||||||
if (!showTitle) {
|
if (!showTitle) {
|
||||||
GsIconButton(
|
GsIconButton(
|
||||||
MediaIcons.Previous, "Previous", media::previous,
|
MediaIcons.Previous, stringResource(R.string.player_previous), media::previous,
|
||||||
enabled = now.canDo(PlaybackState.ACTION_SKIP_TO_PREVIOUS), size = 36.dp, iconSize = 22.dp,
|
enabled = now.canDo(PlaybackState.ACTION_SKIP_TO_PREVIOUS), size = 36.dp, iconSize = 22.dp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
GsIconButton(
|
GsIconButton(
|
||||||
if (now.playing) MediaIcons.Pause else Icons.Filled.PlayArrow,
|
if (now.playing) MediaIcons.Pause else Icons.Filled.PlayArrow,
|
||||||
if (now.playing) "Pause" else "Play",
|
stringResource(if (now.playing) R.string.player_pause else R.string.media_play),
|
||||||
media::togglePlay,
|
media::togglePlay,
|
||||||
tone = GsTone.Primary, size = 36.dp, iconSize = 22.dp,
|
tone = GsTone.Primary, size = 36.dp, iconSize = 22.dp,
|
||||||
)
|
)
|
||||||
if (!showTitle) {
|
if (!showTitle) {
|
||||||
GsIconButton(
|
GsIconButton(
|
||||||
MediaIcons.Next, "Next", media::next,
|
MediaIcons.Next, stringResource(R.string.player_next), media::next,
|
||||||
enabled = now.canDo(PlaybackState.ACTION_SKIP_TO_NEXT), size = 36.dp, iconSize = 22.dp,
|
enabled = now.canDo(PlaybackState.ACTION_SKIP_TO_NEXT), size = 36.dp, iconSize = 22.dp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -633,7 +635,7 @@ private fun NavNowPlaying(navigator: CarNavigator, selected: Boolean, modifier:
|
|||||||
docked -> MediaIcons.DockRightOpen
|
docked -> MediaIcons.DockRightOpen
|
||||||
else -> MediaIcons.DockRight
|
else -> MediaIcons.DockRight
|
||||||
},
|
},
|
||||||
if (docked) "Close the side panel" else "Move the player beside the map",
|
stringResource(if (docked) R.string.navbar_close_panel else R.string.navbar_dock_player),
|
||||||
navigator::togglePlayerDock,
|
navigator::togglePlayerDock,
|
||||||
size = 36.dp, iconSize = 22.dp,
|
size = 36.dp, iconSize = 22.dp,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -41,7 +45,11 @@ sealed interface CarScreen {
|
|||||||
enum class SidePanel { NONE, CONTROLS, LYRICS }
|
enum class SidePanel { NONE, CONTROLS, LYRICS }
|
||||||
|
|
||||||
/** One step on the breadcrumb trail: the screen it shows and the word for it in the trail. */
|
/** One step on the breadcrumb trail: the screen it shows and the word for it in the trail. */
|
||||||
data class Crumb(val screen: CarScreen, val label: String)
|
data class Crumb(val screen: CarScreen, val label: String, @param:StringRes val labelRes: Int = 0) {
|
||||||
|
/** [labelRes] when the step is one of Gearslip's own, translated; [label] for an app's own name. */
|
||||||
|
@Composable
|
||||||
|
fun shown(): String = if (labelRes != 0) stringResource(labelRes) else label
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Where the car UI is, and how it got there. A car touchscreen has no hardware back button, so
|
* Where the car UI is, and how it got there. A car touchscreen has no hardware back button, so
|
||||||
@@ -57,6 +65,7 @@ class CarNavigator(startId: String) {
|
|||||||
Crumb(
|
Crumb(
|
||||||
if (CarApps.find(startId) != null) CarScreen.App(startId) else CarScreen.Home,
|
if (CarApps.find(startId) != null) CarScreen.App(startId) else CarScreen.Home,
|
||||||
"Home",
|
"Home",
|
||||||
|
R.string.crumb_home,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -69,15 +78,15 @@ class CarNavigator(startId: String) {
|
|||||||
|
|
||||||
/** Pushes a new step, unless it's the same screen already on top - tapping the button for
|
/** Pushes a new step, unless it's the same screen already on top - tapping the button for
|
||||||
* where you already are shouldn't grow the trail. */
|
* where you already are shouldn't grow the trail. */
|
||||||
private fun push(screen: CarScreen, label: String) {
|
private fun push(screen: CarScreen, label: String, @StringRes labelRes: Int = 0) {
|
||||||
if (stack.last().screen == screen) return
|
if (stack.last().screen == screen) return
|
||||||
stack.add(Crumb(screen, label))
|
stack.add(Crumb(screen, label, labelRes))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The one way back to a clean slate: resets the whole trail to just Home. */
|
/** The one way back to a clean slate: resets the whole trail to just Home. */
|
||||||
fun home() {
|
fun home() {
|
||||||
stack.clear()
|
stack.clear()
|
||||||
stack.add(Crumb(CarScreen.Home, "Home"))
|
stack.add(Crumb(CarScreen.Home, "Home", R.string.crumb_home))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,17 +126,17 @@ class CarNavigator(startId: String) {
|
|||||||
|
|
||||||
fun dismissAppMenu() { appMenu = null }
|
fun dismissAppMenu() { appMenu = null }
|
||||||
|
|
||||||
fun media() { push(CarScreen.Media, "Media") }
|
fun media() { push(CarScreen.Media, "Media", R.string.crumb_media) }
|
||||||
|
|
||||||
fun browse(label: String) { push(CarScreen.Browse, label) }
|
fun browse(label: String) { push(CarScreen.Browse, label) }
|
||||||
|
|
||||||
fun apps() { push(CarScreen.Apps, "Apps") }
|
fun apps() { push(CarScreen.Apps, "Apps", R.string.crumb_apps) }
|
||||||
|
|
||||||
fun settings() { push(CarScreen.Settings, "Settings") }
|
fun settings() { push(CarScreen.Settings, "Settings", R.string.settings_title) }
|
||||||
|
|
||||||
fun dashboard() { push(CarScreen.Dashboard, "Dashboard") }
|
fun dashboard() { push(CarScreen.Dashboard, "Dashboard", R.string.crumb_dashboard) }
|
||||||
|
|
||||||
fun weather() { push(CarScreen.Weather, "Weather") }
|
fun weather() { push(CarScreen.Weather, "Weather", R.string.crumb_weather) }
|
||||||
|
|
||||||
fun vehicleData() { push(CarScreen.VehicleData, "Vehicle data") }
|
fun vehicleData() { push(CarScreen.VehicleData, "Vehicle data") }
|
||||||
|
|
||||||
@@ -135,10 +144,10 @@ class CarNavigator(startId: String) {
|
|||||||
|
|
||||||
/** The list lives on the dashboard; only a reply gets a screen of its own. */
|
/** The list lives on the dashboard; only a reply gets a screen of its own. */
|
||||||
fun notifications(replyTo: String? = null) {
|
fun notifications(replyTo: String? = null) {
|
||||||
if (replyTo == null) dashboard() else push(CarScreen.Notifications(replyTo), "Reply")
|
if (replyTo == null) dashboard() else push(CarScreen.Notifications(replyTo), "Reply", R.string.crumb_reply)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun open(id: String) { push(CarScreen.App(id), CarApps.find(id)?.label ?: id) }
|
fun open(id: String) { push(CarScreen.App(id), CarApps.find(id)?.label ?: id, CarApps.find(id)?.labelRes ?: 0) }
|
||||||
|
|
||||||
/** Jumps straight to a step already on the trail, dropping everything after it - what
|
/** Jumps straight to a step already on the trail, dropping everything after it - what
|
||||||
* tapping a breadcrumb does. Index 0 (Home) is always on the trail, so this always succeeds. */
|
* tapping a breadcrumb does. Index 0 (Home) is always on the trail, so this always succeeds. */
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
|
|
||||||
import app.seb3thehacker.gearslip.car.theme.DarkStyle
|
import app.seb3thehacker.gearslip.car.theme.DarkStyle
|
||||||
import app.seb3thehacker.gearslip.car.theme.*
|
import app.seb3thehacker.gearslip.car.theme.*
|
||||||
@@ -90,7 +92,7 @@ fun CarSettingsScreen() {
|
|||||||
val listState = androidx.compose.foundation.lazy.rememberLazyListState()
|
val listState = androidx.compose.foundation.lazy.rememberLazyListState()
|
||||||
androidx.compose.runtime.LaunchedEffect(chosen) { listState.scrollToItem(0) }
|
androidx.compose.runtime.LaunchedEffect(chosen) { listState.scrollToItem(0) }
|
||||||
Row(Modifier.fillMaxSize().padding(start = 16.dp, top = 12.dp, bottom = 12.dp)) {
|
Row(Modifier.fillMaxSize().padding(start = 16.dp, top = 12.dp, bottom = 12.dp)) {
|
||||||
SettingsRail(listOf("Screen", "Nav bar and player", "Sound and voice", "Warnings", "More"), chosen) { chosen = it }
|
SettingsRail(listOf(stringResource(R.string.car_group_screen), stringResource(R.string.car_group_nav_bar), stringResource(R.string.car_group_sound), stringResource(R.string.car_group_warnings), stringResource(R.string.car_group_more)), chosen) { chosen = it }
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
Modifier.weight(1f).fillMaxHeight(),
|
Modifier.weight(1f).fillMaxHeight(),
|
||||||
state = listState,
|
state = listState,
|
||||||
@@ -101,40 +103,40 @@ fun CarSettingsScreen() {
|
|||||||
) {
|
) {
|
||||||
var section: Int
|
var section: Int
|
||||||
section = 0
|
section = 0
|
||||||
if (section == chosen) item { Group("Screen") }
|
if (section == chosen) item { Group(stringResource(R.string.car_group_screen)) }
|
||||||
|
|
||||||
if (section == chosen) item {
|
if (section == chosen) item {
|
||||||
ChoiceRow(
|
ChoiceRow(
|
||||||
"Open on connect",
|
stringResource(R.string.car_open_on_connect),
|
||||||
listOf(CarSettings.HOME to "Home") + CarApps.all.filter { it.id != "settings" }.map { it.id to it.label },
|
listOf(CarSettings.HOME to stringResource(R.string.choice_open_home)) + CarApps.all.filter { it.id != "settings" }.map { it.id to stringResource(it.labelRes) },
|
||||||
open,
|
open,
|
||||||
) { CarSettings.setOpenOnConnect(it) }
|
) { CarSettings.setOpenOnConnect(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (section == chosen) item {
|
if (section == chosen) item {
|
||||||
ChoiceRow(
|
ChoiceRow(
|
||||||
"Size",
|
stringResource(R.string.car_size),
|
||||||
listOf(1f to "Normal", 1.25f to "Large", 1.5f to "Extra large"),
|
listOf(1f to stringResource(R.string.choice_size_normal), 1.25f to stringResource(R.string.choice_size_large), 1.5f to stringResource(R.string.choice_size_xlarge)),
|
||||||
scale,
|
scale,
|
||||||
) { CarSettings.setScale(it) }
|
) { CarSettings.setScale(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (section == chosen) item {
|
if (section == chosen) item {
|
||||||
ChoiceRow(
|
ChoiceRow(
|
||||||
"App theme",
|
stringResource(R.string.car_app_theme),
|
||||||
listOf(AppTheme.PHONE to "Follow phone", AppTheme.LIGHT to "Light", AppTheme.DARK to "Dark"),
|
listOf(AppTheme.PHONE to stringResource(R.string.choice_theme_phone), AppTheme.LIGHT to stringResource(R.string.choice_theme_light), AppTheme.DARK to stringResource(R.string.choice_theme_dark)),
|
||||||
theme,
|
theme,
|
||||||
) { CarSettings.setAppTheme(it) }
|
) { CarSettings.setAppTheme(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (section == chosen && theme != AppTheme.LIGHT) item {
|
if (section == chosen && theme != AppTheme.LIGHT) item {
|
||||||
ChoiceRow(
|
ChoiceRow(
|
||||||
"Dark mode",
|
stringResource(R.string.car_dark_mode),
|
||||||
listOf(
|
listOf(
|
||||||
DarkStyle.BLACK to "Black",
|
DarkStyle.BLACK to stringResource(R.string.choice_dark_black),
|
||||||
DarkStyle.BLACK_COLOUR to "Black and colour",
|
DarkStyle.BLACK_COLOUR to stringResource(R.string.choice_dark_black_colour),
|
||||||
DarkStyle.DEEP_COLOUR to "Deep colour",
|
DarkStyle.DEEP_COLOUR to stringResource(R.string.choice_dark_deep_colour),
|
||||||
DarkStyle.GREY to "Grey",
|
DarkStyle.GREY to stringResource(R.string.choice_dark_grey),
|
||||||
),
|
),
|
||||||
darkStyle,
|
darkStyle,
|
||||||
) { CarSettings.setDarkStyle(it) }
|
) { CarSettings.setDarkStyle(it) }
|
||||||
@@ -142,7 +144,7 @@ fun CarSettingsScreen() {
|
|||||||
|
|
||||||
if (section == chosen) item {
|
if (section == chosen) item {
|
||||||
ChoiceRow(
|
ChoiceRow(
|
||||||
"Theme",
|
stringResource(R.string.carset_theme),
|
||||||
GsThemes.builtIn.map { it.id to it.name },
|
GsThemes.builtIn.map { it.id to it.name },
|
||||||
uiTheme,
|
uiTheme,
|
||||||
) { CarSettings.setUiTheme(it) }
|
) { CarSettings.setUiTheme(it) }
|
||||||
@@ -150,16 +152,16 @@ fun CarSettingsScreen() {
|
|||||||
|
|
||||||
if (section == chosen) item {
|
if (section == chosen) item {
|
||||||
ChoiceRow(
|
ChoiceRow(
|
||||||
"Map day/night",
|
stringResource(R.string.car_map_day_night),
|
||||||
listOf(NightMode.AUTO to "Auto", NightMode.DAY to "Day", NightMode.NIGHT to "Night"),
|
listOf(NightMode.AUTO to stringResource(R.string.choice_auto), NightMode.DAY to stringResource(R.string.choice_night_day), NightMode.NIGHT to stringResource(R.string.choice_night_night)),
|
||||||
night,
|
night,
|
||||||
) { CarSettings.setNightMode(it) }
|
) { CarSettings.setNightMode(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (section == chosen) item {
|
if (section == chosen) item {
|
||||||
ChoiceRow(
|
ChoiceRow(
|
||||||
"Units",
|
stringResource(R.string.car_units),
|
||||||
listOf(Units.AUTO to "Auto", Units.IMPERIAL to "Imperial (mi, ft, °F)", Units.METRIC to "Metric (km, m, °C)"),
|
listOf(Units.AUTO to stringResource(R.string.choice_auto), Units.IMPERIAL to stringResource(R.string.carset_imperial), Units.METRIC to stringResource(R.string.carset_metric)),
|
||||||
units,
|
units,
|
||||||
) { CarSettings.setUnits(it) }
|
) { CarSettings.setUnits(it) }
|
||||||
}
|
}
|
||||||
@@ -167,31 +169,31 @@ fun CarSettingsScreen() {
|
|||||||
// For a head unit that reports the wrong size and stretches the picture (issue #12).
|
// For a head unit that reports the wrong size and stretches the picture (issue #12).
|
||||||
if (section == chosen) item {
|
if (section == chosen) item {
|
||||||
ChoiceRow(
|
ChoiceRow(
|
||||||
"Screen size (next connection)",
|
stringResource(R.string.carset_screen_size),
|
||||||
listOf<ScreenSize?>(null).plus(SCREEN_PRESETS).plus(screenSize).distinct()
|
listOf<ScreenSize?>(null).plus(SCREEN_PRESETS).plus(screenSize).distinct()
|
||||||
.map { it to (it?.toString() ?: "From the car") },
|
.map { it to (it?.toString() ?: stringResource(R.string.screen_from_car)) },
|
||||||
screenSize,
|
screenSize,
|
||||||
) { CarSettings.setScreenSize(it) }
|
) { CarSettings.setScreenSize(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (section == chosen && screenSize != null) item {
|
if (section == chosen && screenSize != null) item {
|
||||||
ChoiceRow(
|
ChoiceRow(
|
||||||
"Fit to the screen",
|
stringResource(R.string.carset_fit),
|
||||||
listOf(ScreenFit.CROP to "Crop the edges", ScreenFit.SQUASH to "Squash to fit"),
|
listOf(ScreenFit.CROP to stringResource(R.string.screen_crop), ScreenFit.SQUASH to stringResource(R.string.screen_squash)),
|
||||||
screenFit,
|
screenFit,
|
||||||
) { CarSettings.setScreenFit(it) }
|
) { CarSettings.setScreenFit(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
section = 1
|
section = 1
|
||||||
if (section == chosen) item { Group("Nav bar and player") }
|
if (section == chosen) item { Group(stringResource(R.string.car_group_nav_bar)) }
|
||||||
|
|
||||||
if (section == chosen) item {
|
if (section == chosen) item {
|
||||||
ChoiceRow(
|
ChoiceRow(
|
||||||
"Nav bar",
|
stringResource(R.string.car_nav_bar),
|
||||||
listOf(
|
listOf(
|
||||||
NavBarMode.SHOW to "Always show",
|
NavBarMode.SHOW to stringResource(R.string.choice_nav_show),
|
||||||
NavBarMode.HIDE_NAVIGATING to "Hide while navigating",
|
NavBarMode.HIDE_NAVIGATING to stringResource(R.string.choice_nav_hide_navigating),
|
||||||
NavBarMode.HIDE to "Hide until tapped",
|
NavBarMode.HIDE to stringResource(R.string.choice_nav_hide),
|
||||||
),
|
),
|
||||||
navBar,
|
navBar,
|
||||||
) { CarSettings.setNavBarMode(it) }
|
) { CarSettings.setNavBarMode(it) }
|
||||||
@@ -199,88 +201,85 @@ fun CarSettingsScreen() {
|
|||||||
|
|
||||||
if (section == chosen) item {
|
if (section == chosen) item {
|
||||||
ChoiceRow(
|
ChoiceRow(
|
||||||
"Player in the nav bar",
|
stringResource(R.string.car_player_nav_bar),
|
||||||
listOf(false to "Skip buttons", true to "Song title"),
|
listOf(false to stringResource(R.string.car_skip_buttons), true to stringResource(R.string.car_song_title)),
|
||||||
playerTitle,
|
playerTitle,
|
||||||
) { CarSettings.setPlayerTitle(it) }
|
) { CarSettings.setPlayerTitle(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (section == chosen) item {
|
if (section == chosen) item {
|
||||||
ChoiceRow(
|
ChoiceRow(
|
||||||
"Player beside the map",
|
stringResource(R.string.car_player_beside_map),
|
||||||
listOf(false to "Right", true to "Left"),
|
listOf(false to stringResource(R.string.car_right), true to stringResource(R.string.car_left)),
|
||||||
playerLeft,
|
playerLeft,
|
||||||
) { CarSettings.setPlayerLeft(it) }
|
) { CarSettings.setPlayerLeft(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (section == chosen) item {
|
if (section == chosen) item {
|
||||||
ChoiceRow(
|
ChoiceRow(
|
||||||
"Autoplay on connect",
|
stringResource(R.string.car_autoplay),
|
||||||
listOf(false to "Off", true to "On"),
|
listOf(false to stringResource(R.string.off), true to stringResource(R.string.on)),
|
||||||
autoplay,
|
autoplay,
|
||||||
) { CarSettings.setAutoplay(it) }
|
) { CarSettings.setAutoplay(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (section == chosen) item {
|
if (section == chosen) item {
|
||||||
ChoiceRow(
|
ChoiceRow(
|
||||||
"Lyrics from",
|
stringResource(R.string.carset_lyrics_from),
|
||||||
listOf(false to "LRCLIB", true to "More sources"),
|
listOf(false to "LRCLIB", true to stringResource(R.string.carset_more_sources)),
|
||||||
moreLyrics,
|
moreLyrics,
|
||||||
) { CarSettings.setMoreLyrics(it) }
|
) { CarSettings.setMoreLyrics(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
section = 2
|
section = 2
|
||||||
if (section == chosen) item { Group("Sound and voice") }
|
if (section == chosen) item { Group(stringResource(R.string.car_group_sound)) }
|
||||||
|
|
||||||
if (section == chosen) item {
|
if (section == chosen) item {
|
||||||
ChoiceRow(
|
ChoiceRow(
|
||||||
"Car audio",
|
stringResource(R.string.car_audio),
|
||||||
listOf(false to "Bluetooth", true to "USB"),
|
listOf(false to stringResource(R.string.guide_audio_bluetooth), true to stringResource(R.string.guide_audio_usb)),
|
||||||
pipeAudio,
|
pipeAudio,
|
||||||
) { CarSettings.setPipeAudio(it) }
|
) { CarSettings.setPipeAudio(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (section == chosen) item {
|
if (section == chosen) item {
|
||||||
Text(
|
Text(
|
||||||
"USB sends the phone's sound through Gearslip. Android asks to share your screen each " +
|
stringResource(R.string.carset_audio_note),
|
||||||
"drive, and a few apps block it. Bluetooth plays as the phone normally would, and " +
|
|
||||||
"Android never asks.",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
section = 3
|
section = 3
|
||||||
if (section == chosen) item { Group("Warnings") }
|
if (section == chosen) item { Group(stringResource(R.string.car_group_warnings)) }
|
||||||
|
|
||||||
if (section == chosen && !pipeAudio) item {
|
if (section == chosen && !pipeAudio) item {
|
||||||
ChoiceRow(
|
ChoiceRow(
|
||||||
"Warn when Bluetooth isn't connected",
|
stringResource(R.string.car_warn_bluetooth),
|
||||||
listOf(false to "Off", true to "On"),
|
listOf(false to stringResource(R.string.off), true to stringResource(R.string.on)),
|
||||||
bluetoothWarning,
|
bluetoothWarning,
|
||||||
) { CarSettings.setBluetoothWarning(it) }
|
) { CarSettings.setBluetoothWarning(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (section == chosen) item {
|
if (section == chosen) item {
|
||||||
ChoiceRow(
|
ChoiceRow(
|
||||||
"Warn when location is off",
|
stringResource(R.string.car_warn_location),
|
||||||
listOf(false to "Off", true to "On"),
|
listOf(false to stringResource(R.string.off), true to stringResource(R.string.on)),
|
||||||
locationWarning,
|
locationWarning,
|
||||||
) { CarSettings.setLocationWarning(it) }
|
) { CarSettings.setLocationWarning(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (section == chosen) item {
|
if (section == chosen) item {
|
||||||
ChoiceRow(
|
ChoiceRow(
|
||||||
"Spoken directions",
|
stringResource(R.string.car_spoken_directions),
|
||||||
listOf(false to "Off", true to "On"),
|
listOf(false to stringResource(R.string.off), true to stringResource(R.string.on)),
|
||||||
speakDirections,
|
speakDirections,
|
||||||
) { CarSettings.setSpeakDirections(it) }
|
) { CarSettings.setSpeakDirections(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (section == chosen) item {
|
if (section == chosen) item {
|
||||||
Text(
|
Text(
|
||||||
"Gearslip reads each turn aloud through the car's speakers, and the music dips while it " +
|
stringResource(R.string.carset_directions_note),
|
||||||
"speaks. Turn it off if your map app's own voice already plays in the car.",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
@@ -288,16 +287,14 @@ fun CarSettingsScreen() {
|
|||||||
|
|
||||||
if (section == chosen) item {
|
if (section == chosen) item {
|
||||||
ExperimentalFeatureRow(
|
ExperimentalFeatureRow(
|
||||||
title = "Use the phone's speech recognition",
|
title = stringResource(R.string.car_phone_speech),
|
||||||
description = "Listens through the phone's own speech service instead of Gearslip's. On " +
|
description = stringResource(R.string.carset_phone_speech_note),
|
||||||
"most phones that's Google's, which may send your voice to Google. GrapheneOS has " +
|
|
||||||
"none, so Gearslip keeps using its own.",
|
|
||||||
checked = systemSpeech,
|
checked = systemSpeech,
|
||||||
) { CarSettings.setSystemSpeech(it) }
|
) { CarSettings.setSystemSpeech(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
section = 4
|
section = 4
|
||||||
if (section == chosen) item { Group("More") }
|
if (section == chosen) item { Group(stringResource(R.string.car_group_more)) }
|
||||||
|
|
||||||
if (section == chosen) item {
|
if (section == chosen) item {
|
||||||
WebPageSetting()
|
WebPageSetting()
|
||||||
@@ -309,8 +306,8 @@ fun CarSettingsScreen() {
|
|||||||
|
|
||||||
if (section == chosen) item {
|
if (section == chosen) item {
|
||||||
ChoiceRow(
|
ChoiceRow(
|
||||||
"Experimental features",
|
stringResource(R.string.car_experimental),
|
||||||
listOf(false to "Hide", true to "Show"),
|
listOf(false to stringResource(R.string.carset_hide), true to stringResource(R.string.carset_show)),
|
||||||
experimentalFeatures,
|
experimentalFeatures,
|
||||||
) { CarSettings.setExperimentalFeaturesEnabled(it) }
|
) { CarSettings.setExperimentalFeaturesEnabled(it) }
|
||||||
}
|
}
|
||||||
@@ -321,26 +318,26 @@ fun CarSettingsScreen() {
|
|||||||
|
|
||||||
if (section == chosen) item {
|
if (section == chosen) item {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Text("Connection", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary)
|
Text(stringResource(R.string.settings_connection), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary)
|
||||||
Info("Phone", "${Build.MANUFACTURER} ${Build.MODEL}")
|
Info(stringResource(R.string.carset_phone), "${Build.MANUFACTURER} ${Build.MODEL}")
|
||||||
Info("Vehicle", vehicle.ifEmpty { "-" })
|
Info(stringResource(R.string.carset_vehicle), vehicle.ifEmpty { "-" })
|
||||||
Info("Video", display.ifEmpty { "-" })
|
Info(stringResource(R.string.carset_video), display.ifEmpty { "-" })
|
||||||
Info("Certificate", cert?.headline ?: "Checking…")
|
Info(stringResource(R.string.settings_certificate), cert?.headline ?: stringResource(R.string.cert_checking))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (section == chosen) item {
|
if (section == chosen) item {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Text("About", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary)
|
Text(stringResource(R.string.settings_about), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary)
|
||||||
Info("Version", app.seb3thehacker.gearslip.BuildConfig.VERSION_NAME)
|
Info(stringResource(R.string.carset_version), app.seb3thehacker.gearslip.BuildConfig.VERSION_NAME)
|
||||||
Info("Build", app.seb3thehacker.gearslip.BuildConfig.VERSION_CODE.toString())
|
Info(stringResource(R.string.carset_build), app.seb3thehacker.gearslip.BuildConfig.VERSION_CODE.toString())
|
||||||
ExperimentalFeatureRow(
|
ExperimentalFeatureRow(
|
||||||
"What's new after updates",
|
stringResource(R.string.carset_whats_new_after),
|
||||||
"Show the list of changes once, the first time the car connects after an update.",
|
stringResource(R.string.carset_whats_new_after_sub),
|
||||||
whatsNew,
|
whatsNew,
|
||||||
) { CarSettings.setWhatsNewAfterUpdates(it) }
|
) { CarSettings.setWhatsNewAfterUpdates(it) }
|
||||||
GsButton(onClick = CarSettings::openWhatsNew, tone = GsTone.Tonal, modifier = Modifier.height(56.dp)) {
|
GsButton(onClick = CarSettings::openWhatsNew, tone = GsTone.Tonal, modifier = Modifier.height(56.dp)) {
|
||||||
Text("What's new", style = MaterialTheme.typography.titleMedium)
|
Text(stringResource(R.string.settings_whats_new), style = MaterialTheme.typography.titleMedium)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -381,7 +378,7 @@ private fun SettingsRail(groups: List<String>, chosen: Int, onChoose: (Int) -> U
|
|||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
"Settings",
|
stringResource(R.string.settings_title),
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
modifier = Modifier.padding(start = 14.dp, top = 6.dp, bottom = 10.dp),
|
modifier = Modifier.padding(start = 14.dp, top = 6.dp, bottom = 10.dp),
|
||||||
@@ -454,17 +451,13 @@ private fun ExperimentalFeaturesList(carSensors: Boolean, adaptiveVideo: Boolean
|
|||||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
) {
|
) {
|
||||||
ExperimentalFeatureRow(
|
ExperimentalFeatureRow(
|
||||||
title = "Vehicle data",
|
title = stringResource(R.string.car_vehicle_data),
|
||||||
description = "Adds a Vehicle data tile: a live, raw dump of the head unit's sensor " +
|
description = stringResource(R.string.carset_vehicle_data_note),
|
||||||
"channel. Field labels (speed, RPM, fuel...) are carried over from other " +
|
|
||||||
"reverse-engineering projects and unverified against this car - treat them as a " +
|
|
||||||
"guess, not a fact, until you've watched one change for yourself.",
|
|
||||||
checked = carSensors,
|
checked = carSensors,
|
||||||
) { CarSettings.setCarSensorsEnabled(it) }
|
) { CarSettings.setCarSensorsEnabled(it) }
|
||||||
ExperimentalFeatureRow(
|
ExperimentalFeatureRow(
|
||||||
title = "Adaptive video quality",
|
title = stringResource(R.string.car_adaptive_video),
|
||||||
description = "Lowers the picture's quality when the car, or its Wi-Fi, can't keep " +
|
description = stringResource(R.string.carset_adaptive_note),
|
||||||
"up, and raises it again once it can. Takes effect on the next connection.",
|
|
||||||
checked = adaptiveVideo,
|
checked = adaptiveVideo,
|
||||||
) { CarSettings.setAdaptiveVideoEnabled(it) }
|
) { CarSettings.setAdaptiveVideoEnabled(it) }
|
||||||
}
|
}
|
||||||
@@ -512,7 +505,7 @@ private fun WebPageSetting() {
|
|||||||
var url by remember { mutableStateOf(AppSettings.startupUrl(context)) }
|
var url by remember { mutableStateOf(AppSettings.startupUrl(context)) }
|
||||||
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Text("Web page", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary)
|
Text(stringResource(R.string.web_page), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary)
|
||||||
Surface(
|
Surface(
|
||||||
onClick = { editing = !editing },
|
onClick = { editing = !editing },
|
||||||
shape = MaterialTheme.shapes.large,
|
shape = MaterialTheme.shapes.large,
|
||||||
@@ -520,7 +513,7 @@ private fun WebPageSetting() {
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
url.ifEmpty { "DuckDuckGo (default) - tap to set a different page" },
|
url.ifEmpty { stringResource(R.string.carset_web_default) },
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = if (url.isEmpty()) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurface,
|
color = if (url.isEmpty()) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurface,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
@@ -529,7 +522,7 @@ private fun WebPageSetting() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
"Shown by the Browser app on the launcher. Applies on the next connection.",
|
stringResource(R.string.carset_web_note),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
@@ -572,22 +565,22 @@ private fun ScreenFit(vehicle: String) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Text("Screen fit", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary)
|
Text(stringResource(R.string.carset_screen_fit), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary)
|
||||||
Stepper("Top", insets.top) { change(insets.copy(top = (insets.top + it).coerceAtLeast(0))) }
|
Stepper(stringResource(R.string.carset_top), insets.top) { change(insets.copy(top = (insets.top + it).coerceAtLeast(0))) }
|
||||||
Stepper("Bottom", insets.bottom) { change(insets.copy(bottom = (insets.bottom + it).coerceAtLeast(0))) }
|
Stepper(stringResource(R.string.carset_bottom), insets.bottom) { change(insets.copy(bottom = (insets.bottom + it).coerceAtLeast(0))) }
|
||||||
Text(
|
Text(
|
||||||
"Usable height ${480 - insets.top - insets.bottom}px of 480",
|
stringResource(R.string.carset_usable_height, 480 - insets.top - insets.bottom),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
GsButton(
|
GsButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
val ok = app.seb3thehacker.gearslip.VehicleProfiles.saveInsets(context, vehicle, insets)
|
val ok = app.seb3thehacker.gearslip.VehicleProfiles.saveInsets(context, vehicle, insets)
|
||||||
saved = if (ok) "Saved to $vehicle" else "Could not save"
|
saved = if (ok) context.getString(R.string.carset_saved_to, vehicle) else context.getString(R.string.carset_save_failed)
|
||||||
},
|
},
|
||||||
enabled = known,
|
enabled = known,
|
||||||
modifier = Modifier.fillMaxWidth().height(56.dp),
|
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||||
) { Text(if (known) "Save for this vehicle" else "No vehicle profile matched") }
|
) { Text(if (known) stringResource(R.string.carset_save_vehicle) else stringResource(R.string.carset_no_profile)) }
|
||||||
saved?.let { Text(it, style = MaterialTheme.typography.bodyMedium) }
|
saved?.let { Text(it, style = MaterialTheme.typography.bodyMedium) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -599,7 +592,7 @@ private fun Stepper(label: String, value: Int, onStep: (Int) -> Unit) {
|
|||||||
StepButton("−5", Modifier.weight(1f)) { onStep(-5) }
|
StepButton("−5", Modifier.weight(1f)) { onStep(-5) }
|
||||||
StepButton("−1", Modifier.weight(1f)) { onStep(-1) }
|
StepButton("−1", Modifier.weight(1f)) { onStep(-1) }
|
||||||
Text(
|
Text(
|
||||||
"${value}px",
|
stringResource(R.string.carset_px, value),
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
modifier = Modifier.weight(1.3f),
|
modifier = Modifier.weight(1.3f),
|
||||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
|
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.material.icons.filled.Call
|
import androidx.compose.material.icons.filled.Call
|
||||||
import app.seb3thehacker.gearslip.call.CarCalls
|
import app.seb3thehacker.gearslip.call.CarCalls
|
||||||
import app.seb3thehacker.gearslip.call.CallUiState
|
import app.seb3thehacker.gearslip.call.CallUiState
|
||||||
@@ -359,7 +360,7 @@ fun CarUi() {
|
|||||||
GsButton(onClick = CarFullscreen::exit, modifier = Modifier.height(48.dp)) {
|
GsButton(onClick = CarFullscreen::exit, modifier = Modifier.height(48.dp)) {
|
||||||
Icon(MediaIcons.FullscreenExit, contentDescription = null, modifier = Modifier.size(22.dp))
|
Icon(MediaIcons.FullscreenExit, contentDescription = null, modifier = Modifier.size(22.dp))
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Text("Exit full screen", style = MaterialTheme.typography.titleMedium)
|
Text(stringResource(R.string.carui_exit_fullscreen), style = MaterialTheme.typography.titleMedium)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -404,22 +405,20 @@ private fun CarSafetyWizard(onDone: () -> Unit) {
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Column(Modifier.padding(24.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
Column(Modifier.padding(24.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
Text("Drive safely", style = ChromeType.headline, fontWeight = FontWeight.Bold)
|
Text(stringResource(R.string.carui_drive_safely), style = ChromeType.headline, fontWeight = FontWeight.Bold)
|
||||||
Text(
|
Text(
|
||||||
"Gearslip is unofficial software: it is not made or reviewed by any " +
|
stringResource(R.string.carui_unofficial),
|
||||||
"carmaker, and it can fail without warning.",
|
|
||||||
style = ChromeType.body,
|
style = ChromeType.body,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
"Never interact with this screen while driving. Set the route or the " +
|
stringResource(R.string.carui_never_interact),
|
||||||
"song before you go, then keep your attention on the road.",
|
|
||||||
style = ChromeType.body,
|
style = ChromeType.body,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(24.dp))
|
Spacer(Modifier.height(24.dp))
|
||||||
GsButton(onClick = onDone, modifier = Modifier.fillMaxWidth().height(56.dp)) {
|
GsButton(onClick = onDone, modifier = Modifier.fillMaxWidth().height(56.dp)) {
|
||||||
Text("I understand", style = ChromeType.body)
|
Text(stringResource(R.string.carui_understand), style = ChromeType.body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -450,7 +449,7 @@ private fun BrowseAppScreen(navigator: CarNavigator) {
|
|||||||
}
|
}
|
||||||
val failed = status.phase == CarAppConnection.Phase.REJECTED || status.phase == CarAppConnection.Phase.FAILED
|
val failed = status.phase == CarAppConnection.Phase.REJECTED || status.phase == CarAppConnection.Phase.FAILED
|
||||||
if (failed || timedOut) {
|
if (failed || timedOut) {
|
||||||
AppWontStart(status.app ?: "This app", leave)
|
AppWontStart(status.app ?: stringResource(R.string.carui_this_app), leave)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
CarAppStage(CarServices.browse, status, frame, onDisconnect = leave)
|
CarAppStage(CarServices.browse, status, frame, onDisconnect = leave)
|
||||||
@@ -466,13 +465,13 @@ private fun AppWontStart(app: String, onBack: () -> Unit) {
|
|||||||
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically),
|
verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
Text("$app doesn't work with Gearslip yet.", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold)
|
Text(stringResource(R.string.carui_app_unsupported, app), style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.SemiBold)
|
||||||
Text(
|
Text(
|
||||||
"It didn't start. Some apps only run on Google's Android Auto.",
|
stringResource(R.string.carui_app_unsupported_body),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
GsButton(onClick = onBack, tone = GsTone.Tonal) { Text("Back") }
|
GsButton(onClick = onBack, tone = GsTone.Tonal) { Text(stringResource(R.string.back)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
@@ -76,7 +78,7 @@ fun CarWhatsNew() {
|
|||||||
) {
|
) {
|
||||||
Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(14.dp)) {
|
Column(Modifier.padding(20.dp), verticalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||||
Text(
|
Text(
|
||||||
"What's new in ${release.versionName}",
|
stringResource(R.string.whatsnew_title, release.versionName),
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
)
|
||||||
@@ -99,12 +101,12 @@ fun CarWhatsNew() {
|
|||||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(14.dp)) {
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(14.dp)) {
|
||||||
GsSwitch(afterUpdates, CarSettings::setWhatsNewAfterUpdates)
|
GsSwitch(afterUpdates, CarSettings::setWhatsNewAfterUpdates)
|
||||||
Text(
|
Text(
|
||||||
"Show after updates",
|
stringResource(R.string.whatsnew_show_after),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
GsButton(onClick = close, modifier = Modifier.height(56.dp)) {
|
GsButton(onClick = close, modifier = Modifier.height(56.dp)) {
|
||||||
Text("Got it", style = MaterialTheme.typography.titleMedium)
|
Text(stringResource(R.string.whatsnew_got_it), style = MaterialTheme.typography.titleMedium)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.speech.tts.TextToSpeech
|
import android.speech.tts.TextToSpeech
|
||||||
@@ -121,7 +122,7 @@ object DirectionsVoice {
|
|||||||
say(instruction)
|
say(instruction)
|
||||||
stage = Stage.DONE
|
stage = Stage.DONE
|
||||||
} else {
|
} else {
|
||||||
say("In ${spoken(turn.distance)}, $instruction")
|
say(ahead(turn.distance, instruction))
|
||||||
stage = if (meters != null && meters > PREPARE_FROM_METERS) Stage.FAR else Stage.PREPARED
|
stage = if (meters != null && meters > PREPARE_FROM_METERS) Stage.FAR else Stage.PREPARED
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -132,7 +133,7 @@ object DirectionsVoice {
|
|||||||
stage = Stage.DONE
|
stage = Stage.DONE
|
||||||
}
|
}
|
||||||
stage == Stage.FAR && meters != null && meters <= PREPARE_AT_METERS -> {
|
stage == Stage.FAR && meters != null && meters <= PREPARE_AT_METERS -> {
|
||||||
say("In ${spoken(turn.distance)}, $instruction")
|
say(ahead(turn.distance, instruction))
|
||||||
stage = Stage.PREPARED
|
stage = Stage.PREPARED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,67 +141,79 @@ object DirectionsVoice {
|
|||||||
|
|
||||||
/** What to do, in words: built from the manoeuvre when the app names one, else the app's own text. */
|
/** What to do, in words: built from the manoeuvre when the app names one, else the app's own text. */
|
||||||
private fun instruction(step: Step): String? {
|
private fun instruction(step: Step): String? {
|
||||||
|
val res = appContext?.resources ?: return null
|
||||||
val road = step.road.text()
|
val road = step.road.text()
|
||||||
val onto = if (road.isBlank()) "" else " onto $road"
|
// Each manoeuvre is a whole sentence, with and without the road, so a translation can
|
||||||
val toward = if (road.isBlank()) "" else " toward $road"
|
// put the road wherever its language wants it.
|
||||||
|
fun say(plain: Int, withRoad: Int) =
|
||||||
|
if (road.isBlank() || withRoad == 0) res.getString(plain) else res.getString(withRoad, road)
|
||||||
val maneuver = step.maneuver
|
val maneuver = step.maneuver
|
||||||
val built = when (maneuver?.type) {
|
val built = when (maneuver?.type) {
|
||||||
Maneuver.TYPE_TURN_SLIGHT_LEFT -> "Turn slightly left$onto"
|
Maneuver.TYPE_TURN_SLIGHT_LEFT -> say(R.string.dir_slight_left, R.string.dir_slight_left_onto)
|
||||||
Maneuver.TYPE_TURN_SLIGHT_RIGHT -> "Turn slightly right$onto"
|
Maneuver.TYPE_TURN_SLIGHT_RIGHT -> say(R.string.dir_slight_right, R.string.dir_slight_right_onto)
|
||||||
Maneuver.TYPE_TURN_NORMAL_LEFT -> "Turn left$onto"
|
Maneuver.TYPE_TURN_NORMAL_LEFT -> say(R.string.dir_left, R.string.dir_left_onto)
|
||||||
Maneuver.TYPE_TURN_NORMAL_RIGHT -> "Turn right$onto"
|
Maneuver.TYPE_TURN_NORMAL_RIGHT -> say(R.string.dir_right, R.string.dir_right_onto)
|
||||||
Maneuver.TYPE_TURN_SHARP_LEFT -> "Turn sharply left$onto"
|
Maneuver.TYPE_TURN_SHARP_LEFT -> say(R.string.dir_sharp_left, R.string.dir_sharp_left_onto)
|
||||||
Maneuver.TYPE_TURN_SHARP_RIGHT -> "Turn sharply right$onto"
|
Maneuver.TYPE_TURN_SHARP_RIGHT -> say(R.string.dir_sharp_right, R.string.dir_sharp_right_onto)
|
||||||
Maneuver.TYPE_U_TURN_LEFT, Maneuver.TYPE_U_TURN_RIGHT -> "Make a U-turn"
|
Maneuver.TYPE_U_TURN_LEFT, Maneuver.TYPE_U_TURN_RIGHT -> say(R.string.dir_u_turn, 0)
|
||||||
Maneuver.TYPE_KEEP_LEFT -> "Keep left$toward"
|
Maneuver.TYPE_KEEP_LEFT -> say(R.string.dir_keep_left, R.string.dir_keep_left_toward)
|
||||||
Maneuver.TYPE_KEEP_RIGHT -> "Keep right$toward"
|
Maneuver.TYPE_KEEP_RIGHT -> say(R.string.dir_keep_right, R.string.dir_keep_right_toward)
|
||||||
Maneuver.TYPE_FORK_LEFT -> "Keep left at the fork$toward"
|
Maneuver.TYPE_FORK_LEFT -> say(R.string.dir_fork_left, R.string.dir_fork_left_toward)
|
||||||
Maneuver.TYPE_FORK_RIGHT -> "Keep right at the fork$toward"
|
Maneuver.TYPE_FORK_RIGHT -> say(R.string.dir_fork_right, R.string.dir_fork_right_toward)
|
||||||
Maneuver.TYPE_ON_RAMP_SLIGHT_LEFT, Maneuver.TYPE_ON_RAMP_NORMAL_LEFT, Maneuver.TYPE_ON_RAMP_SHARP_LEFT,
|
Maneuver.TYPE_ON_RAMP_SLIGHT_LEFT, Maneuver.TYPE_ON_RAMP_NORMAL_LEFT, Maneuver.TYPE_ON_RAMP_SHARP_LEFT,
|
||||||
Maneuver.TYPE_ON_RAMP_U_TURN_LEFT -> "Take the ramp on the left$onto"
|
Maneuver.TYPE_ON_RAMP_U_TURN_LEFT -> say(R.string.dir_ramp_left, R.string.dir_ramp_left_onto)
|
||||||
Maneuver.TYPE_ON_RAMP_SLIGHT_RIGHT, Maneuver.TYPE_ON_RAMP_NORMAL_RIGHT, Maneuver.TYPE_ON_RAMP_SHARP_RIGHT,
|
Maneuver.TYPE_ON_RAMP_SLIGHT_RIGHT, Maneuver.TYPE_ON_RAMP_NORMAL_RIGHT, Maneuver.TYPE_ON_RAMP_SHARP_RIGHT,
|
||||||
Maneuver.TYPE_ON_RAMP_U_TURN_RIGHT -> "Take the ramp on the right$onto"
|
Maneuver.TYPE_ON_RAMP_U_TURN_RIGHT -> say(R.string.dir_ramp_right, R.string.dir_ramp_right_onto)
|
||||||
Maneuver.TYPE_OFF_RAMP_SLIGHT_LEFT, Maneuver.TYPE_OFF_RAMP_NORMAL_LEFT -> "Take the exit on the left$toward"
|
Maneuver.TYPE_OFF_RAMP_SLIGHT_LEFT, Maneuver.TYPE_OFF_RAMP_NORMAL_LEFT -> say(R.string.dir_exit_left, R.string.dir_exit_left_toward)
|
||||||
Maneuver.TYPE_OFF_RAMP_SLIGHT_RIGHT, Maneuver.TYPE_OFF_RAMP_NORMAL_RIGHT -> "Take the exit on the right$toward"
|
Maneuver.TYPE_OFF_RAMP_SLIGHT_RIGHT, Maneuver.TYPE_OFF_RAMP_NORMAL_RIGHT -> say(R.string.dir_exit_right, R.string.dir_exit_right_toward)
|
||||||
Maneuver.TYPE_MERGE_LEFT -> "Merge left$onto"
|
Maneuver.TYPE_MERGE_LEFT -> say(R.string.dir_merge_left, R.string.dir_merge_left_onto)
|
||||||
Maneuver.TYPE_MERGE_RIGHT -> "Merge right$onto"
|
Maneuver.TYPE_MERGE_RIGHT -> say(R.string.dir_merge_right, R.string.dir_merge_right_onto)
|
||||||
Maneuver.TYPE_MERGE_SIDE_UNSPECIFIED -> "Merge$onto"
|
Maneuver.TYPE_MERGE_SIDE_UNSPECIFIED -> say(R.string.dir_merge, R.string.dir_merge_onto)
|
||||||
Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW, Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE,
|
Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW, Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW_WITH_ANGLE,
|
||||||
Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW, Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE ->
|
Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW, Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CCW_WITH_ANGLE ->
|
||||||
maneuver.roundaboutExitNumber.takeIf { it in 1..ORDINALS.size }
|
maneuver.roundaboutExitNumber.takeIf { it in 1..ORDINALS.size }
|
||||||
?.let { "At the roundabout, take the ${ORDINALS[it - 1]} exit$onto" }
|
?.let { res.getString(ORDINALS[it - 1]) }
|
||||||
?: "Go through the roundabout$onto"
|
?.let { exit ->
|
||||||
Maneuver.TYPE_ROUNDABOUT_ENTER_CW, Maneuver.TYPE_ROUNDABOUT_ENTER_CCW -> "Enter the roundabout"
|
if (road.isBlank()) res.getString(R.string.dir_roundabout_take, exit)
|
||||||
Maneuver.TYPE_ROUNDABOUT_EXIT_CW, Maneuver.TYPE_ROUNDABOUT_EXIT_CCW -> "Exit the roundabout$onto"
|
else res.getString(R.string.dir_roundabout_take_onto, exit, road)
|
||||||
Maneuver.TYPE_STRAIGHT -> "Continue straight$onto"
|
}
|
||||||
Maneuver.TYPE_NAME_CHANGE -> "Continue$onto"
|
?: say(R.string.dir_roundabout_through, R.string.dir_roundabout_through_onto)
|
||||||
Maneuver.TYPE_FERRY_BOAT, Maneuver.TYPE_FERRY_BOAT_LEFT, Maneuver.TYPE_FERRY_BOAT_RIGHT -> "Take the ferry"
|
Maneuver.TYPE_ROUNDABOUT_ENTER_CW, Maneuver.TYPE_ROUNDABOUT_ENTER_CCW -> say(R.string.dir_roundabout_enter, 0)
|
||||||
Maneuver.TYPE_FERRY_TRAIN, Maneuver.TYPE_FERRY_TRAIN_LEFT, Maneuver.TYPE_FERRY_TRAIN_RIGHT -> "Take the train"
|
Maneuver.TYPE_ROUNDABOUT_EXIT_CW, Maneuver.TYPE_ROUNDABOUT_EXIT_CCW -> say(R.string.dir_roundabout_exit, R.string.dir_roundabout_exit_onto)
|
||||||
Maneuver.TYPE_DESTINATION, Maneuver.TYPE_DESTINATION_STRAIGHT -> "Your destination is ahead"
|
Maneuver.TYPE_STRAIGHT -> say(R.string.dir_straight, R.string.dir_straight_onto)
|
||||||
Maneuver.TYPE_DESTINATION_LEFT -> "Your destination is on the left"
|
Maneuver.TYPE_NAME_CHANGE -> say(R.string.dir_continue, R.string.dir_continue_onto)
|
||||||
Maneuver.TYPE_DESTINATION_RIGHT -> "Your destination is on the right"
|
Maneuver.TYPE_FERRY_BOAT, Maneuver.TYPE_FERRY_BOAT_LEFT, Maneuver.TYPE_FERRY_BOAT_RIGHT -> say(R.string.dir_ferry, 0)
|
||||||
|
Maneuver.TYPE_FERRY_TRAIN, Maneuver.TYPE_FERRY_TRAIN_LEFT, Maneuver.TYPE_FERRY_TRAIN_RIGHT -> say(R.string.dir_train, 0)
|
||||||
|
Maneuver.TYPE_DESTINATION, Maneuver.TYPE_DESTINATION_STRAIGHT -> say(R.string.dir_destination, 0)
|
||||||
|
Maneuver.TYPE_DESTINATION_LEFT -> say(R.string.dir_destination_left, 0)
|
||||||
|
Maneuver.TYPE_DESTINATION_RIGHT -> say(R.string.dir_destination_right, 0)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
return built ?: step.cue.text().ifBlank { null }
|
return built ?: step.cue.text().ifBlank { null }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** "In 300 meters, turn left": the distance ahead of a turn that isn't close yet. */
|
||||||
|
private fun ahead(d: Distance, instruction: String): String =
|
||||||
|
appContext?.getString(R.string.dir_in_distance, spoken(d), instruction) ?: instruction
|
||||||
|
|
||||||
/** The app's own rounded distance, read the way a person would say it. */
|
/** The app's own rounded distance, read the way a person would say it. */
|
||||||
private fun spoken(d: Distance): String {
|
private fun spoken(d: Distance): String {
|
||||||
|
val res = appContext?.resources ?: return ""
|
||||||
val n = d.displayDistance
|
val n = d.displayDistance
|
||||||
val number = if (n % 1.0 == 0.0) n.toInt().toString() else "%.1f".format(n)
|
val whole = n % 1.0 == 0.0
|
||||||
val one = n == 1.0
|
val number = if (whole) n.toInt().toString() else "%.1f".format(n)
|
||||||
return when (d.displayUnit) {
|
return when (d.displayUnit) {
|
||||||
Distance.UNIT_KILOMETERS, Distance.UNIT_KILOMETERS_P1 -> if (one) "1 kilometer" else "$number kilometers"
|
Distance.UNIT_KILOMETERS, Distance.UNIT_KILOMETERS_P1 ->
|
||||||
|
if (whole) res.getQuantityString(R.plurals.dir_km, n.toInt(), number) else res.getString(R.string.dir_km_decimal, number)
|
||||||
Distance.UNIT_MILES, Distance.UNIT_MILES_P1 -> when (n) {
|
Distance.UNIT_MILES, Distance.UNIT_MILES_P1 -> when (n) {
|
||||||
0.25 -> "a quarter mile"
|
0.25 -> res.getString(R.string.dir_quarter_mile)
|
||||||
0.5 -> "half a mile"
|
0.5 -> res.getString(R.string.dir_half_mile)
|
||||||
0.75 -> "three quarters of a mile"
|
0.75 -> res.getString(R.string.dir_three_quarter_mile)
|
||||||
1.0 -> "1 mile"
|
else -> if (whole) res.getQuantityString(R.plurals.dir_miles, n.toInt(), number) else res.getString(R.string.dir_miles_decimal, number)
|
||||||
else -> "$number miles"
|
|
||||||
}
|
}
|
||||||
Distance.UNIT_FEET -> "$number feet"
|
Distance.UNIT_FEET -> res.getQuantityString(R.plurals.dir_feet, n.toInt(), number)
|
||||||
Distance.UNIT_YARDS -> "$number yards"
|
Distance.UNIT_YARDS -> res.getQuantityString(R.plurals.dir_yards, n.toInt(), number)
|
||||||
else -> "${n.roundToInt()} meters"
|
else -> res.getQuantityString(R.plurals.dir_meters, n.roundToInt(), n.roundToInt())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,5 +360,8 @@ object DirectionsVoice {
|
|||||||
private const val LEAD_MS = 100L
|
private const val LEAD_MS = 100L
|
||||||
private const val HOLD_MS = 2_000L
|
private const val HOLD_MS = 2_000L
|
||||||
private const val SYNTH_TIMEOUT_S = 10L
|
private const val SYNTH_TIMEOUT_S = 10L
|
||||||
private val ORDINALS = listOf("first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth")
|
private val ORDINALS = listOf(
|
||||||
|
R.string.dir_ordinal_1, R.string.dir_ordinal_2, R.string.dir_ordinal_3, R.string.dir_ordinal_4,
|
||||||
|
R.string.dir_ordinal_5, R.string.dir_ordinal_6, R.string.dir_ordinal_7, R.string.dir_ordinal_8,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import app.seb3thehacker.gearslip.car.theme.*
|
import app.seb3thehacker.gearslip.car.theme.*
|
||||||
@@ -86,7 +88,7 @@ internal fun BrowsePanel(browse: app.seb3thehacker.gearslip.media.BrowseState, m
|
|||||||
if (media.canGoUp || browse.trail.isNotEmpty()) {
|
if (media.canGoUp || browse.trail.isNotEmpty()) {
|
||||||
Row(Modifier.padding(bottom = 12.dp), verticalAlignment = Alignment.CenterVertically) {
|
Row(Modifier.padding(bottom = 12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
if (media.canGoUp) {
|
if (media.canGoUp) {
|
||||||
GsIconButton(Icons.Filled.ArrowBack, "Up", media::up, size = 44.dp, shape = CircleShape)
|
GsIconButton(Icons.Filled.ArrowBack, stringResource(R.string.media_up), media::up, size = 44.dp, shape = CircleShape)
|
||||||
Spacer(Modifier.width(14.dp))
|
Spacer(Modifier.width(14.dp))
|
||||||
}
|
}
|
||||||
browse.trail.lastOrNull()?.let {
|
browse.trail.lastOrNull()?.let {
|
||||||
@@ -99,13 +101,13 @@ internal fun BrowsePanel(browse: app.seb3thehacker.gearslip.media.BrowseState, m
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
when {
|
when {
|
||||||
browse.loading -> Notice("Loading…", null)
|
browse.loading -> Notice(stringResource(R.string.media_loading), null)
|
||||||
browse.unavailable -> Notice(
|
browse.unavailable -> Notice(
|
||||||
"$appLabel keeps its library to itself.",
|
stringResource(R.string.media_keeps_library, appLabel),
|
||||||
"Start music in the app on your phone. Play, pause, skip and seek work from here.",
|
stringResource(R.string.media_browse_start),
|
||||||
)
|
)
|
||||||
browse.failed -> Notice("Could not load this list.", null)
|
browse.failed -> Notice(stringResource(R.string.media_load_failed), null)
|
||||||
browse.entries.isEmpty() -> Notice("Nothing here.", null)
|
browse.entries.isEmpty() -> Notice(stringResource(R.string.media_nothing_here), null)
|
||||||
else -> BrowseList(browse.entries, media)
|
else -> BrowseList(browse.entries, media)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,9 +230,8 @@ internal fun QueuePanel(queue: List<QueueTrack>, now: NowPlaying, media: CarMedi
|
|||||||
Column(modifier) {
|
Column(modifier) {
|
||||||
if (upNext.isEmpty()) {
|
if (upNext.isEmpty()) {
|
||||||
Notice(
|
Notice(
|
||||||
"Nothing queued.",
|
stringResource(R.string.media_nothing_queued),
|
||||||
"${if (now.title.isEmpty()) "Nothing playing" else "Only what's already playing"} - " +
|
stringResource(if (now.title.isEmpty()) R.string.media_queue_empty_idle else R.string.media_queue_empty_playing),
|
||||||
"not every app tells Gearslip what comes next.",
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
LazyColumn(Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
@@ -260,7 +261,7 @@ private fun QueueRow(track: QueueTrack, media: CarMedia) {
|
|||||||
Text(track.subtitle, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
Text(track.subtitle, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Icon(Icons.Filled.PlayArrow, "Play", tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
Icon(Icons.Filled.PlayArrow, stringResource(R.string.media_play), tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -286,7 +287,7 @@ internal fun EntryRow(entry: MediaEntry, media: CarMedia, onClick: () -> Unit =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (entry.browsable && entry.playable) {
|
if (entry.browsable && entry.playable) {
|
||||||
GsIconButton(Icons.Filled.PlayArrow, "Play", { media.playAll(entry) })
|
GsIconButton(Icons.Filled.PlayArrow, stringResource(R.string.media_play), { media.playAll(entry) })
|
||||||
}
|
}
|
||||||
if (entry.browsable) Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, null)
|
if (entry.browsable) Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, null)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import app.seb3thehacker.gearslip.car.theme.*
|
import app.seb3thehacker.gearslip.car.theme.*
|
||||||
@@ -110,10 +112,9 @@ fun MediaScreen(app: MediaApp, onExit: () -> Unit) {
|
|||||||
when (phase) {
|
when (phase) {
|
||||||
CarMedia.Phase.REJECTED -> when (rejection) {
|
CarMedia.Phase.REJECTED -> when (rejection) {
|
||||||
CarMedia.Rejection.NEEDS_NOTIFICATION_ACCESS -> Notice(
|
CarMedia.Rejection.NEEDS_NOTIFICATION_ACCESS -> Notice(
|
||||||
"${app.label} keeps its library to itself.",
|
stringResource(R.string.media_keeps_library, app.label),
|
||||||
"Gearslip can still control it once it has Notification access on the phone. " +
|
stringResource(R.string.media_needs_access),
|
||||||
"Allow it under Settings > Notifications > Notification access.",
|
action = stringResource(R.string.media_open_on_phone) to {
|
||||||
action = "Open on phone" to {
|
|
||||||
val app = context.applicationContext
|
val app = context.applicationContext
|
||||||
runCatching {
|
runCatching {
|
||||||
app.startActivity(
|
app.startActivity(
|
||||||
@@ -123,17 +124,16 @@ fun MediaScreen(app: MediaApp, onExit: () -> Unit) {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
CarMedia.Rejection.NEEDS_APP_RUNNING -> Notice(
|
CarMedia.Rejection.NEEDS_APP_RUNNING -> Notice(
|
||||||
"${app.label} isn't running.",
|
stringResource(R.string.media_not_running, app.label),
|
||||||
"${app.label} plays through Gearslip's own player, but only while it's " +
|
stringResource(R.string.media_not_running_body, app.label),
|
||||||
"open on the phone. Open it once and it keeps playing here in the car.",
|
action = stringResource(R.string.media_open_app, app.label) to { media.launchAndConnect(app) },
|
||||||
action = "Open ${app.label}" to { media.launchAndConnect(app) },
|
|
||||||
)
|
)
|
||||||
CarMedia.Rejection.REFUSED -> Notice(
|
CarMedia.Rejection.REFUSED -> Notice(
|
||||||
"${app.label} would not let Gearslip browse it.",
|
stringResource(R.string.media_refused, app.label),
|
||||||
"Some media apps only accept Google's own host.",
|
stringResource(R.string.media_refused_body),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> Notice("Connecting to ${app.label}…", null)
|
else -> Notice(stringResource(R.string.media_connecting, app.label), null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return@Surface
|
return@Surface
|
||||||
@@ -180,14 +180,14 @@ fun MediaScreen(app: MediaApp, onExit: () -> Unit) {
|
|||||||
}
|
}
|
||||||
Column(Modifier.weight(1.2f).fillMaxHeight()) {
|
Column(Modifier.weight(1.2f).fillMaxHeight()) {
|
||||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
MediaTab("Now playing", tab == Tab.NOW_PLAYING, Modifier.weight(1f)) { tab = Tab.NOW_PLAYING }
|
MediaTab(stringResource(R.string.media_now_playing), tab == Tab.NOW_PLAYING, Modifier.weight(1f)) { tab = Tab.NOW_PLAYING }
|
||||||
MediaTab("Up next", tab == Tab.QUEUE, Modifier.weight(1f)) { tab = Tab.QUEUE }
|
MediaTab(stringResource(R.string.media_up_next), tab == Tab.QUEUE, Modifier.weight(1f)) { tab = Tab.QUEUE }
|
||||||
MediaTab("Browse", tab == Tab.BROWSE, Modifier.weight(1f)) { tab = Tab.BROWSE }
|
MediaTab(stringResource(R.string.media_browse), tab == Tab.BROWSE, Modifier.weight(1f)) { tab = Tab.BROWSE }
|
||||||
MediaTab("Lyrics", tab == Tab.LYRICS, Modifier.weight(1f)) { tab = Tab.LYRICS }
|
MediaTab(stringResource(R.string.storage_lyrics), tab == Tab.LYRICS, Modifier.weight(1f)) { tab = Tab.LYRICS }
|
||||||
if (now.canSearch || media.canSearchLibrary) {
|
if (now.canSearch || media.canSearchLibrary) {
|
||||||
GsIconButton(
|
GsIconButton(
|
||||||
Icons.Filled.Search,
|
Icons.Filled.Search,
|
||||||
"Search ${app.label}",
|
stringResource(R.string.media_search, app.label),
|
||||||
{ searching = true },
|
{ searching = true },
|
||||||
colors = GsColors(MaterialTheme.colorScheme.surfaceContainerHigh, MaterialTheme.colorScheme.onSurface),
|
colors = GsColors(MaterialTheme.colorScheme.surfaceContainerHigh, MaterialTheme.colorScheme.onSurface),
|
||||||
size = 44.dp,
|
size = 44.dp,
|
||||||
@@ -218,7 +218,7 @@ fun MediaScreen(app: MediaApp, onExit: () -> Unit) {
|
|||||||
private fun AppIdentity(app: MediaApp, onExit: () -> Unit, modifier: Modifier = Modifier) {
|
private fun AppIdentity(app: MediaApp, onExit: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
val icon = remember(app) { findEntry(app.component.flattenToString())?.icon }
|
val icon = remember(app) { findEntry(app.component.flattenToString())?.icon }
|
||||||
Row(modifier, verticalAlignment = Alignment.CenterVertically) {
|
Row(modifier, verticalAlignment = Alignment.CenterVertically) {
|
||||||
GsIconButton(Icons.Filled.ArrowBack, "Back", onExit, size = 44.dp)
|
GsIconButton(Icons.Filled.ArrowBack, stringResource(R.string.back), onExit, size = 44.dp)
|
||||||
Spacer(Modifier.width(14.dp))
|
Spacer(Modifier.width(14.dp))
|
||||||
icon?.let {
|
icon?.let {
|
||||||
Image(it, null, Modifier.size(26.dp).clip(RoundedCornerShape(7.dp)))
|
Image(it, null, Modifier.size(26.dp).clip(RoundedCornerShape(7.dp)))
|
||||||
@@ -278,10 +278,10 @@ private fun SearchPanel(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
GsIconButton(Icons.Filled.ArrowBack, "Back", onClose, size = ICON_BUTTON_SIZE)
|
GsIconButton(Icons.Filled.ArrowBack, stringResource(R.string.back), onClose, size = ICON_BUTTON_SIZE)
|
||||||
SearchField(
|
SearchField(
|
||||||
query,
|
query,
|
||||||
"Search $appLabel",
|
stringResource(R.string.media_search, appLabel),
|
||||||
Modifier.weight(1f).clickable { typing = true },
|
Modifier.weight(1f).clickable { typing = true },
|
||||||
onClear = {
|
onClear = {
|
||||||
query = ""
|
query = ""
|
||||||
@@ -292,12 +292,12 @@ private fun SearchPanel(
|
|||||||
Box(Modifier.weight(1f).fillMaxWidth().padding(horizontal = 16.dp)) {
|
Box(Modifier.weight(1f).fillMaxWidth().padding(horizontal = 16.dp)) {
|
||||||
val entries = current?.entries
|
val entries = current?.entries
|
||||||
when {
|
when {
|
||||||
trimmed.isEmpty() -> SearchNote("Type a song, artist, album, or playlist.")
|
trimmed.isEmpty() -> SearchNote(stringResource(R.string.media_search_hint))
|
||||||
unsupported && canPickForYou ->
|
unsupported && canPickForYou ->
|
||||||
SearchNote("$appLabel can't list results. Press search, and $appLabel picks what to play.")
|
SearchNote(stringResource(R.string.media_search_no_list, appLabel))
|
||||||
unsupported -> SearchNote("$appLabel can't search from the car.")
|
unsupported -> SearchNote(stringResource(R.string.media_search_unsupported, appLabel))
|
||||||
entries == null -> SearchNote("Searching $appLabel…")
|
entries == null -> SearchNote(stringResource(R.string.media_searching, appLabel))
|
||||||
entries.isEmpty() -> SearchNote("$appLabel found nothing for \"$trimmed\".")
|
entries.isEmpty() -> SearchNote(stringResource(R.string.media_search_nothing, appLabel, trimmed))
|
||||||
else -> LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(bottom = 8.dp)) {
|
else -> LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(bottom = 8.dp)) {
|
||||||
items(entries) { entry ->
|
items(entries) { entry ->
|
||||||
EntryRow(entry, media) {
|
EntryRow(entry, media) {
|
||||||
@@ -355,8 +355,8 @@ private fun SearchStatus(search: CarMedia.Search, appLabel: String, onDismiss: (
|
|||||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
if (search.ignored) "$appLabel didn't play anything for \"${search.query}\"."
|
if (search.ignored) stringResource(R.string.media_search_ignored, appLabel, search.query)
|
||||||
else "Searching $appLabel for \"${search.query}\"…",
|
else stringResource(R.string.media_searching_for, appLabel, search.query),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
@@ -411,9 +411,9 @@ internal fun LyricsPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
when (val state = lyrics) {
|
when (val state = lyrics) {
|
||||||
LyricsState.Loading -> Notice("Looking for lyrics…", null)
|
LyricsState.Loading -> Notice(stringResource(R.string.media_looking_lyrics), null)
|
||||||
LyricsState.Missing -> Notice(
|
LyricsState.Missing -> Notice(
|
||||||
if (now.title.isEmpty()) "Nothing playing." else "No lyrics found for this track.",
|
stringResource(if (now.title.isEmpty()) R.string.media_nothing_playing_dot else R.string.media_no_lyrics),
|
||||||
null,
|
null,
|
||||||
)
|
)
|
||||||
is LyricsState.Found -> Column(modifier) {
|
is LyricsState.Found -> Column(modifier) {
|
||||||
@@ -639,7 +639,7 @@ private fun StageControls(now: NowPlaying, media: CarMedia, modifier: Modifier)
|
|||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
now.title.ifEmpty { "Nothing playing" },
|
now.title.ifEmpty { stringResource(R.string.media_nothing_playing) },
|
||||||
style = MaterialTheme.typography.displaySmall, fontWeight = FontWeight.Bold,
|
style = MaterialTheme.typography.displaySmall, fontWeight = FontWeight.Bold,
|
||||||
maxLines = 2, overflow = TextOverflow.Ellipsis,
|
maxLines = 2, overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
@@ -652,7 +652,7 @@ private fun StageControls(now: NowPlaying, media: CarMedia, modifier: Modifier)
|
|||||||
now.error?.let { Text(it, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.error, maxLines = 2) }
|
now.error?.let { Text(it, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.error, maxLines = 2) }
|
||||||
if (now.title.isEmpty() && now.error == null) {
|
if (now.title.isEmpty() && now.error == null) {
|
||||||
Text(
|
Text(
|
||||||
"Press play to pick up where you left off, or find something in Browse.",
|
stringResource(R.string.media_press_play),
|
||||||
style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurfaceVariant,
|
style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -683,7 +683,7 @@ private fun NowPlayingControls(now: NowPlaying, media: CarMedia, modifier: Modif
|
|||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
Text(
|
Text(
|
||||||
now.title.ifEmpty { "Nothing playing" },
|
now.title.ifEmpty { stringResource(R.string.media_nothing_playing) },
|
||||||
style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold,
|
style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold,
|
||||||
maxLines = 2, overflow = TextOverflow.Ellipsis,
|
maxLines = 2, overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
@@ -695,7 +695,7 @@ private fun NowPlayingControls(now: NowPlaying, media: CarMedia, modifier: Modif
|
|||||||
now.error?.let { Text(it, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.error, maxLines = 2) }
|
now.error?.let { Text(it, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.error, maxLines = 2) }
|
||||||
if (now.title.isEmpty() && now.error == null) {
|
if (now.title.isEmpty() && now.error == null) {
|
||||||
Text(
|
Text(
|
||||||
"Press play to pick up where you left off, or find something in Browse.",
|
stringResource(R.string.media_press_play),
|
||||||
style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurfaceVariant,
|
style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -721,7 +721,7 @@ private fun CompactPlayer(now: NowPlaying, media: CarMedia, art: android.graphic
|
|||||||
Spacer(Modifier.width(16.dp))
|
Spacer(Modifier.width(16.dp))
|
||||||
Column(Modifier.weight(1f).padding(bottom = 4.dp)) {
|
Column(Modifier.weight(1f).padding(bottom = 4.dp)) {
|
||||||
Text(
|
Text(
|
||||||
now.title.ifEmpty { "Nothing playing" },
|
now.title.ifEmpty { stringResource(R.string.media_nothing_playing) },
|
||||||
style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold,
|
style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold,
|
||||||
maxLines = 3, overflow = TextOverflow.Ellipsis,
|
maxLines = 3, overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
@@ -83,9 +85,10 @@ internal fun ReplyGlyph(modifier: Modifier = Modifier) =
|
|||||||
Icon(ImageVector.vectorResource(R.drawable.voice_selection_24), null, modifier)
|
Icon(ImageVector.vectorResource(R.drawable.voice_selection_24), null, modifier)
|
||||||
|
|
||||||
/** "Alex", or "Alex · 3 new" when a burst of messages from one chat has been merged. */
|
/** "Alex", or "Alex · 3 new" when a burst of messages from one chat has been merged. */
|
||||||
|
@Composable
|
||||||
private fun CarNotification.heading(): String {
|
private fun CarNotification.heading(): String {
|
||||||
val who = title.ifBlank { appLabel }
|
val who = title.ifBlank { appLabel }
|
||||||
return if (burst > 1) "$who · $burst new" else who
|
return if (burst > 1) pluralStringResource(R.plurals.msg_burst, burst, who, burst) else who
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The card. It shows the voice reply too, wherever that was started from. */
|
/** The card. It shows the voice reply too, wherever that was started from. */
|
||||||
@@ -106,17 +109,17 @@ fun NotificationPopup(showNew: Boolean, modifier: Modifier = Modifier) {
|
|||||||
Text(n.heading(), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
Text(n.heading(), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||||
Text(n.text, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
Text(n.text, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||||
}
|
}
|
||||||
GsIconButton(Icons.Filled.Close, "Dismiss", { CarNotifications.dismissPopup(n.id) }, size = 40.dp, iconSize = 20.dp)
|
GsIconButton(Icons.Filled.Close, stringResource(R.string.msg_dismiss), { CarNotifications.dismissPopup(n.id) }, size = 40.dp, iconSize = 20.dp)
|
||||||
}
|
}
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
GsButton(onClick = { VoiceReply.read(n) }, tone = GsTone.Neutral, modifier = Modifier.weight(1f)) {
|
GsButton(onClick = { VoiceReply.read(n) }, tone = GsTone.Neutral, modifier = Modifier.weight(1f)) {
|
||||||
ReadGlyph(Modifier.size(20.dp))
|
ReadGlyph(Modifier.size(20.dp))
|
||||||
Text("Read", Modifier.padding(start = 8.dp))
|
Text(stringResource(R.string.msg_read), Modifier.padding(start = 8.dp))
|
||||||
}
|
}
|
||||||
if (n.reply != null) {
|
if (n.reply != null) {
|
||||||
GsButton(onClick = { VoiceReply.reply(n) }, modifier = Modifier.weight(1f)) {
|
GsButton(onClick = { VoiceReply.reply(n) }, modifier = Modifier.weight(1f)) {
|
||||||
ReplyGlyph(Modifier.size(20.dp))
|
ReplyGlyph(Modifier.size(20.dp))
|
||||||
Text("Reply", Modifier.padding(start = 8.dp))
|
Text(stringResource(R.string.crumb_reply), Modifier.padding(start = 8.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,36 +194,36 @@ private fun VoiceReplyBody(s: VoiceReply.State, n: CarNotification) {
|
|||||||
when (s.phase) {
|
when (s.phase) {
|
||||||
VoiceReply.Phase.READING, VoiceReply.Phase.READ -> {
|
VoiceReply.Phase.READING, VoiceReply.Phase.READ -> {
|
||||||
GsButton(onClick = VoiceReply::cancel, tone = GsTone.Neutral, modifier = wide) {
|
GsButton(onClick = VoiceReply::cancel, tone = GsTone.Neutral, modifier = wide) {
|
||||||
Text(if (s.phase == VoiceReply.Phase.READING) "Stop" else "Close")
|
Text(if (s.phase == VoiceReply.Phase.READING) stringResource(R.string.msg_stop) else stringResource(R.string.close))
|
||||||
}
|
}
|
||||||
if (n.reply != null) {
|
if (n.reply != null) {
|
||||||
GsButton(onClick = { VoiceReply.reply(n, readFirst = false) }, modifier = wide) {
|
GsButton(onClick = { VoiceReply.reply(n, readFirst = false) }, modifier = wide) {
|
||||||
ReplyGlyph(Modifier.size(20.dp))
|
ReplyGlyph(Modifier.size(20.dp))
|
||||||
Text("Reply", Modifier.padding(start = 8.dp))
|
Text(stringResource(R.string.crumb_reply), Modifier.padding(start = 8.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
VoiceReply.Phase.OFFERING -> {
|
VoiceReply.Phase.OFFERING -> {
|
||||||
GsButton(onClick = VoiceReply::cancel, tone = GsTone.Neutral, modifier = wide) { Text("No") }
|
GsButton(onClick = VoiceReply::cancel, tone = GsTone.Neutral, modifier = wide) { Text(stringResource(R.string.msg_no)) }
|
||||||
GsButton(onClick = { VoiceReply.reply(n, readFirst = false) }, modifier = wide) {
|
GsButton(onClick = { VoiceReply.reply(n, readFirst = false) }, modifier = wide) {
|
||||||
ReplyGlyph(Modifier.size(20.dp))
|
ReplyGlyph(Modifier.size(20.dp))
|
||||||
Text("Reply", Modifier.padding(start = 8.dp))
|
Text(stringResource(R.string.crumb_reply), Modifier.padding(start = 8.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
VoiceReply.Phase.ASKING, VoiceReply.Phase.LISTENING ->
|
VoiceReply.Phase.ASKING, VoiceReply.Phase.LISTENING ->
|
||||||
GsButton(onClick = VoiceReply::cancel, tone = GsTone.Neutral, modifier = wide) { Text("Cancel") }
|
GsButton(onClick = VoiceReply::cancel, tone = GsTone.Neutral, modifier = wide) { Text(stringResource(R.string.cancel)) }
|
||||||
VoiceReply.Phase.CONFIRMING -> {
|
VoiceReply.Phase.CONFIRMING -> {
|
||||||
GsButton(onClick = VoiceReply::cancel, tone = GsTone.Neutral, modifier = wide) { Text("Cancel") }
|
GsButton(onClick = VoiceReply::cancel, tone = GsTone.Neutral, modifier = wide) { Text(stringResource(R.string.cancel)) }
|
||||||
GsButton(onClick = VoiceReply::sendNow, modifier = wide) { Text("Send now") }
|
GsButton(onClick = VoiceReply::sendNow, modifier = wide) { Text(stringResource(R.string.msg_send_now)) }
|
||||||
}
|
}
|
||||||
VoiceReply.Phase.ERROR -> {
|
VoiceReply.Phase.ERROR -> {
|
||||||
GsButton(onClick = VoiceReply::cancel, tone = GsTone.Neutral, modifier = wide) { Text("Close") }
|
GsButton(onClick = VoiceReply::cancel, tone = GsTone.Neutral, modifier = wide) { Text(stringResource(R.string.close)) }
|
||||||
if (n.reply != null) {
|
if (n.reply != null) {
|
||||||
GsButton(onClick = { VoiceReply.reply(n, readFirst = false) }, tone = GsTone.Tonal, modifier = wide) { Text("Try again") }
|
GsButton(onClick = { VoiceReply.reply(n, readFirst = false) }, tone = GsTone.Tonal, modifier = wide) { Text(stringResource(R.string.update_try_again)) }
|
||||||
GsButton(
|
GsButton(
|
||||||
onClick = { VoiceReply.cancel(); navigator.notifications(replyTo = n.key) },
|
onClick = { VoiceReply.cancel(); navigator.notifications(replyTo = n.key) },
|
||||||
tone = GsTone.Neutral, modifier = wide,
|
tone = GsTone.Neutral, modifier = wide,
|
||||||
) { Text("Type") }
|
) { Text(stringResource(R.string.msg_type)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
VoiceReply.Phase.SENT, VoiceReply.Phase.IDLE -> Unit
|
VoiceReply.Phase.SENT, VoiceReply.Phase.IDLE -> Unit
|
||||||
@@ -228,13 +231,14 @@ private fun VoiceReplyBody(s: VoiceReply.State, n: CarNotification) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
private fun voiceHeading(s: VoiceReply.State, who: String): String = when (s.phase) {
|
private fun voiceHeading(s: VoiceReply.State, who: String): String = when (s.phase) {
|
||||||
VoiceReply.Phase.READING, VoiceReply.Phase.READ -> who
|
VoiceReply.Phase.READING, VoiceReply.Phase.READ -> who
|
||||||
VoiceReply.Phase.OFFERING, VoiceReply.Phase.ASKING -> "Reply to $who"
|
VoiceReply.Phase.OFFERING, VoiceReply.Phase.ASKING -> stringResource(R.string.msg_reply_to, who)
|
||||||
VoiceReply.Phase.LISTENING -> "Listening"
|
VoiceReply.Phase.LISTENING -> stringResource(R.string.msg_listening)
|
||||||
VoiceReply.Phase.CONFIRMING -> "Sending to $who"
|
VoiceReply.Phase.CONFIRMING -> stringResource(R.string.msg_sending_to, who)
|
||||||
VoiceReply.Phase.SENT -> "Sent to $who"
|
VoiceReply.Phase.SENT -> stringResource(R.string.msg_sent_to, who)
|
||||||
VoiceReply.Phase.ERROR -> "Couldn't reply"
|
VoiceReply.Phase.ERROR -> stringResource(R.string.msg_reply_failed)
|
||||||
VoiceReply.Phase.IDLE -> ""
|
VoiceReply.Phase.IDLE -> ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import app.seb3thehacker.gearslip.car.theme.*
|
import app.seb3thehacker.gearslip.car.theme.*
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
@@ -60,14 +63,14 @@ fun MessagesScreen(packageName: String) {
|
|||||||
val navigator = LocalCarNavigator.current
|
val navigator = LocalCarNavigator.current
|
||||||
val context = LocalContext.current.applicationContext
|
val context = LocalContext.current.applicationContext
|
||||||
val entry = remember(packageName) { findEntry(MESSAGING_PREFIX + packageName) }
|
val entry = remember(packageName) { findEntry(MESSAGING_PREFIX + packageName) }
|
||||||
val label = entry?.label ?: history.firstOrNull { it.packageName == packageName }?.appLabel ?: "Messages"
|
val label = entry?.label ?: history.firstOrNull { it.packageName == packageName }?.appLabel ?: stringResource(R.string.msg_messages)
|
||||||
val threads = history.filter { it.packageName == packageName }
|
val threads = history.filter { it.packageName == packageName }
|
||||||
|
|
||||||
LaunchedEffect(history) { CarNotifications.dismissPopupFrom(packageName) }
|
LaunchedEffect(history) { CarNotifications.dismissPopupFrom(packageName) }
|
||||||
|
|
||||||
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
GsIconButton(Icons.AutoMirrored.Filled.ArrowBack, "Back", navigator::back)
|
GsIconButton(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back), navigator::back)
|
||||||
entry?.icon?.let { Image(it, contentDescription = null, modifier = Modifier.size(36.dp).clip(RoundedCornerShape(8.dp))) }
|
entry?.icon?.let { Image(it, contentDescription = null, modifier = Modifier.size(36.dp).clip(RoundedCornerShape(8.dp))) }
|
||||||
Text(label, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
Text(label, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||||
}
|
}
|
||||||
@@ -82,7 +85,7 @@ fun MessagesScreen(packageName: String) {
|
|||||||
|
|
||||||
if (threads.isEmpty()) {
|
if (threads.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
"No recent messages. New $label messages appear here as they arrive on the phone.",
|
stringResource(R.string.msg_none_recent, label),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
@@ -146,7 +149,7 @@ private fun ConversationCard(n: CarNotification, onRead: () -> Unit, onReply: ()
|
|||||||
}
|
}
|
||||||
if (!expanded && lines.size > COLLAPSED_LINES) {
|
if (!expanded && lines.size > COLLAPSED_LINES) {
|
||||||
Text(
|
Text(
|
||||||
"${lines.size - COLLAPSED_LINES} earlier",
|
pluralStringResource(R.plurals.msg_earlier, lines.size - COLLAPSED_LINES, lines.size - COLLAPSED_LINES),
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
@@ -156,25 +159,25 @@ private fun ConversationCard(n: CarNotification, onRead: () -> Unit, onReply: ()
|
|||||||
}
|
}
|
||||||
Column(horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
if (n.replied) {
|
if (n.replied) {
|
||||||
Icon(Icons.Filled.Check, contentDescription = "Replied", tint = MaterialTheme.colorScheme.primary)
|
Icon(Icons.Filled.Check, contentDescription = stringResource(R.string.notif_replied), tint = MaterialTheme.colorScheme.primary)
|
||||||
}
|
}
|
||||||
GsButton(onClick = onRead, tone = GsTone.Neutral) {
|
GsButton(onClick = onRead, tone = GsTone.Neutral) {
|
||||||
ReadGlyph(Modifier.size(20.dp))
|
ReadGlyph(Modifier.size(20.dp))
|
||||||
Text("Read", Modifier.padding(start = 8.dp))
|
Text(stringResource(R.string.msg_read), Modifier.padding(start = 8.dp))
|
||||||
}
|
}
|
||||||
if (n.reply != null) {
|
if (n.reply != null) {
|
||||||
GsButton(onClick = onReply, tone = GsTone.Tonal) {
|
GsButton(onClick = onReply, tone = GsTone.Tonal) {
|
||||||
ReplyGlyph(Modifier.size(20.dp))
|
ReplyGlyph(Modifier.size(20.dp))
|
||||||
Text("Reply", Modifier.padding(start = 8.dp))
|
Text(stringResource(R.string.crumb_reply), Modifier.padding(start = 8.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (n.reply != null) {
|
if (n.reply != null) {
|
||||||
GsButton(onClick = { quick = !quick }, tone = GsTone.Neutral) {
|
GsButton(onClick = { quick = !quick }, tone = GsTone.Neutral) {
|
||||||
Text(if (quick) "Hide quick replies" else "Quick reply")
|
Text(stringResource(if (quick) R.string.msg_hide_quick else R.string.msg_quick))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (n.markRead != null) {
|
if (n.markRead != null) {
|
||||||
GsButton(onClick = onMarkRead, tone = GsTone.Neutral) { Text("Mark read") }
|
GsButton(onClick = onMarkRead, tone = GsTone.Neutral) { Text(stringResource(R.string.msg_mark_read)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,8 +187,8 @@ private fun ConversationCard(n: CarNotification, onRead: () -> Unit, onReply: ()
|
|||||||
@Composable
|
@Composable
|
||||||
private fun MessageLine(line: ChatLine, showSender: Boolean) {
|
private fun MessageLine(line: ChatLine, showSender: Boolean) {
|
||||||
val who = when {
|
val who = when {
|
||||||
line.sender == null -> "You: "
|
line.sender == null -> stringResource(R.string.msg_you)
|
||||||
showSender -> "${line.sender}: "
|
showSender -> stringResource(R.string.msg_sender, line.sender)
|
||||||
else -> ""
|
else -> ""
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@@ -65,26 +68,32 @@ private val CarGlyph: ImageVector = ImageVector.Builder("Car", 24.dp, 24.dp, 24f
|
|||||||
*/
|
*/
|
||||||
internal class BuiltInApp(
|
internal class BuiltInApp(
|
||||||
val id: String,
|
val id: String,
|
||||||
|
/** English, for matching what a driver says; [labelRes] is what the screen shows. */
|
||||||
val label: String,
|
val label: String,
|
||||||
|
@param:StringRes val labelRes: Int,
|
||||||
val glyph: ImageVector,
|
val glyph: ImageVector,
|
||||||
val screen: CarScreen?,
|
val screen: CarScreen?,
|
||||||
val open: (CarNavigator) -> Unit,
|
val open: (CarNavigator) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
internal object BuiltInApps {
|
internal object BuiltInApps {
|
||||||
val Web = BuiltInApp("web", "Browser", Icons.Filled.Search, CarScreen.App("web")) { it.open("web") }
|
val Web = BuiltInApp("web", "Browser", R.string.app_browser, Icons.Filled.Search, CarScreen.App("web")) { it.open("web") }
|
||||||
val ScreenSharing = BuiltInApp("phone", "Screen sharing", Icons.Filled.Share, CarScreen.App("phone")) { it.open("phone") }
|
val ScreenSharing = BuiltInApp("phone", "Screen sharing", R.string.app_screen_sharing, Icons.Filled.Share, CarScreen.App("phone")) { it.open("phone") }
|
||||||
val Phone = BuiltInApp("dialer", "Phone", Icons.Filled.Call, CarScreen.App("dialer")) { it.open("dialer") }
|
val Phone = BuiltInApp("dialer", "Phone", R.string.carset_phone, Icons.Filled.Call, CarScreen.App("dialer")) { it.open("dialer") }
|
||||||
val VehicleData = BuiltInApp("vehicle", "Vehicle data", Icons.Filled.Info, CarScreen.VehicleData) { it.vehicleData() }
|
val VehicleData = BuiltInApp("vehicle", "Vehicle data", R.string.car_vehicle_data, Icons.Filled.Info, CarScreen.VehicleData) { it.vehicleData() }
|
||||||
val Settings = BuiltInApp("settings", "Settings", Icons.Filled.Settings, CarScreen.Settings) { it.settings() }
|
val Settings = BuiltInApp("settings", "Settings", R.string.settings_title, Icons.Filled.Settings, CarScreen.Settings) { it.settings() }
|
||||||
|
|
||||||
/** Hands the screen back to the car's own interface, like Android Auto's Exit. */
|
/** Hands the screen back to the car's own interface, like Android Auto's Exit. */
|
||||||
val Exit = BuiltInApp("exit", "Exit", CarGlyph, screen = null) { CarFocus.exitToCar() }
|
val Exit = BuiltInApp("exit", "Exit", R.string.app_exit, CarGlyph, screen = null) { CarFocus.exitToCar() }
|
||||||
|
|
||||||
val all = listOf(Web, ScreenSharing, Phone, VehicleData, Settings, Exit)
|
val all = listOf(Web, ScreenSharing, Phone, VehicleData, Settings, Exit)
|
||||||
val entries: List<Entry> = all.map { Entry(it.label, icon = null, builtIn = it) }
|
val entries: List<Entry> = all.map { Entry(it.label, icon = null, builtIn = it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The name a driver sees: translated for Gearslip's own apps, the app's own name otherwise. */
|
||||||
|
@Composable
|
||||||
|
internal fun Entry.shownLabel(): String = builtIn?.let { stringResource(it.labelRes) } ?: label
|
||||||
|
|
||||||
/** Vehicle data is experimental: raw, unverified sensor labels (see [CarSensors]). */
|
/** Vehicle data is experimental: raw, unverified sensor labels (see [CarSensors]). */
|
||||||
@Composable
|
@Composable
|
||||||
internal fun rememberVehicleDataShown(): Boolean {
|
internal fun rememberVehicleDataShown(): Boolean {
|
||||||
@@ -215,7 +224,7 @@ internal fun AppMenu(navigator: CarNavigator, running: Map<String, RunningSlot>)
|
|||||||
entry.icon?.let { Image(it, null, Modifier.size(44.dp).clip(RoundedCornerShape(10.dp))) }
|
entry.icon?.let { Image(it, null, Modifier.size(44.dp).clip(RoundedCornerShape(10.dp))) }
|
||||||
entry.builtIn?.let { Icon(it.glyph, null, Modifier.size(36.dp)) }
|
entry.builtIn?.let { Icon(it.glyph, null, Modifier.size(36.dp)) }
|
||||||
Text(
|
Text(
|
||||||
entry.label,
|
entry.shownLabel(),
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
modifier = Modifier.padding(start = 14.dp),
|
modifier = Modifier.padding(start = 14.dp),
|
||||||
@@ -232,9 +241,9 @@ internal fun AppMenu(navigator: CarNavigator, running: Map<String, RunningSlot>)
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
when {
|
when {
|
||||||
isPinned -> "Unpin from nav bar"
|
isPinned -> stringResource(R.string.navapps_unpin)
|
||||||
barFull -> "Nav bar is full (${CarSettings.MAX_PINNED} pinned)"
|
barFull -> stringResource(R.string.navapps_full, CarSettings.MAX_PINNED)
|
||||||
else -> "Pin to nav bar"
|
else -> stringResource(R.string.navapps_pin)
|
||||||
},
|
},
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
)
|
)
|
||||||
@@ -248,13 +257,13 @@ internal fun AppMenu(navigator: CarNavigator, running: Map<String, RunningSlot>)
|
|||||||
},
|
},
|
||||||
tone = GsTone.Danger,
|
tone = GsTone.Danger,
|
||||||
modifier = Modifier.fillMaxWidth().height(56.dp),
|
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||||
) { Text("Close app", style = MaterialTheme.typography.titleMedium) }
|
) { Text(stringResource(R.string.navapps_close), style = MaterialTheme.typography.titleMedium) }
|
||||||
}
|
}
|
||||||
GsButton(
|
GsButton(
|
||||||
onClick = navigator::dismissAppMenu,
|
onClick = navigator::dismissAppMenu,
|
||||||
tone = GsTone.Neutral,
|
tone = GsTone.Neutral,
|
||||||
modifier = Modifier.fillMaxWidth().height(56.dp),
|
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||||
) { Text("Cancel", style = MaterialTheme.typography.titleMedium) }
|
) { Text(stringResource(R.string.cancel), style = MaterialTheme.typography.titleMedium) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,19 +290,19 @@ private fun MoveButtons(id: String) {
|
|||||||
tone = GsTone.Tonal,
|
tone = GsTone.Tonal,
|
||||||
enabled = index > 0,
|
enabled = index > 0,
|
||||||
modifier = Modifier.weight(1f).height(56.dp),
|
modifier = Modifier.weight(1f).height(56.dp),
|
||||||
) { Text("Move left", style = MaterialTheme.typography.titleMedium) }
|
) { Text(stringResource(R.string.navapps_move_left), style = MaterialTheme.typography.titleMedium) }
|
||||||
GsButton(
|
GsButton(
|
||||||
onClick = { move(1) },
|
onClick = { move(1) },
|
||||||
tone = GsTone.Tonal,
|
tone = GsTone.Tonal,
|
||||||
enabled = index < order.lastIndex,
|
enabled = index < order.lastIndex,
|
||||||
modifier = Modifier.weight(1f).height(56.dp),
|
modifier = Modifier.weight(1f).height(56.dp),
|
||||||
) { Text("Move right", style = MaterialTheme.typography.titleMedium) }
|
) { Text(stringResource(R.string.navapps_move_right), style = MaterialTheme.typography.titleMedium) }
|
||||||
}
|
}
|
||||||
if (custom.isNotEmpty()) {
|
if (custom.isNotEmpty()) {
|
||||||
GsButton(
|
GsButton(
|
||||||
onClick = { CarSettings.setAppOrder(emptyList()) },
|
onClick = { CarSettings.setAppOrder(emptyList()) },
|
||||||
tone = GsTone.Neutral,
|
tone = GsTone.Neutral,
|
||||||
modifier = Modifier.fillMaxWidth().height(56.dp),
|
modifier = Modifier.fillMaxWidth().height(56.dp),
|
||||||
) { Text("Reset app order", style = MaterialTheme.typography.titleMedium) }
|
) { Text(stringResource(R.string.navapps_reset), style = MaterialTheme.typography.titleMedium) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import app.seb3thehacker.gearslip.car.theme.*
|
import app.seb3thehacker.gearslip.car.theme.*
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
@@ -79,12 +81,12 @@ fun NotificationsSection() {
|
|||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(
|
Text(
|
||||||
"Notifications",
|
stringResource(R.string.notif_title),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
if (history.isNotEmpty()) TextButton(onClick = CarNotifications::clear) { Text("Clear all") }
|
if (history.isNotEmpty()) TextButton(onClick = CarNotifications::clear) { Text(stringResource(R.string.notif_clear_all)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!listening) {
|
if (!listening) {
|
||||||
@@ -97,7 +99,7 @@ fun NotificationsSection() {
|
|||||||
|
|
||||||
if (history.isEmpty()) {
|
if (history.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
"Nothing yet. New notifications from the phone will show up here.",
|
stringResource(R.string.notif_empty),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
@@ -124,11 +126,11 @@ internal fun AccessBanner(onOpen: () -> Unit) {
|
|||||||
) {
|
) {
|
||||||
Icon(Icons.Filled.Warning, contentDescription = null)
|
Icon(Icons.Filled.Warning, contentDescription = null)
|
||||||
Text(
|
Text(
|
||||||
"Notifications are off. Allow Gearslip under Settings > Notifications > Notification access on the phone.",
|
stringResource(R.string.notif_off),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
GsButton(onClick = onOpen) { Text("Open") }
|
GsButton(onClick = onOpen) { Text(stringResource(R.string.guide_open)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,10 +162,10 @@ private fun NotificationRow(n: CarNotification, onReply: (CarNotification) -> Un
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
when {
|
when {
|
||||||
n.replied -> Icon(Icons.Filled.Check, contentDescription = "Replied", tint = MaterialTheme.colorScheme.primary)
|
n.replied -> Icon(Icons.Filled.Check, contentDescription = stringResource(R.string.notif_replied), tint = MaterialTheme.colorScheme.primary)
|
||||||
n.reply != null -> GsButton(onClick = { VoiceReply.reply(n) }, tone = GsTone.Tonal) {
|
n.reply != null -> GsButton(onClick = { VoiceReply.reply(n) }, tone = GsTone.Tonal) {
|
||||||
ReplyGlyph(Modifier.size(20.dp))
|
ReplyGlyph(Modifier.size(20.dp))
|
||||||
Text("Reply", Modifier.padding(start = 8.dp))
|
Text(stringResource(R.string.crumb_reply), Modifier.padding(start = 8.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,14 +200,14 @@ private fun ReplyPane(n: CarNotification, onDone: () -> Unit) {
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
GsIconButton(Icons.AutoMirrored.Filled.ArrowBack, "Back", onDone)
|
GsIconButton(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back), onDone)
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(24.dp),
|
shape = RoundedCornerShape(24.dp),
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerHighest,
|
color = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
if (text.isEmpty()) "Reply to ${n.title.ifBlank { n.appLabel }}" else "$text|",
|
if (text.isEmpty()) stringResource(R.string.msg_reply_to, n.title.ifBlank { n.appLabel }) else "$text|",
|
||||||
color = if (text.isEmpty()) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurface,
|
color = if (text.isEmpty()) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurface,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import app.seb3thehacker.gearslip.car.theme.*
|
import app.seb3thehacker.gearslip.car.theme.*
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -53,7 +55,7 @@ fun PhoneAppsScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(Modifier.fillMaxSize().padding(horizontal = 20.dp, vertical = 12.dp)) {
|
Column(Modifier.fillMaxSize().padding(horizontal = 20.dp, vertical = 12.dp)) {
|
||||||
ScreenHeader("Screen sharing")
|
ScreenHeader(stringResource(R.string.app_screen_sharing))
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
if (!TouchRelayService.enabled) {
|
if (!TouchRelayService.enabled) {
|
||||||
TouchRelayWarning()
|
TouchRelayWarning()
|
||||||
@@ -94,11 +96,11 @@ private fun TouchRelayWarning() {
|
|||||||
) {
|
) {
|
||||||
Icon(Icons.Filled.Warning, contentDescription = null)
|
Icon(Icons.Filled.Warning, contentDescription = null)
|
||||||
Text(
|
Text(
|
||||||
"Touch is off. Enable Gearslip under Settings > Accessibility on the phone.",
|
stringResource(R.string.phoneapps_touch_off),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
GsButton(onClick = { PhoneApps.openAccessibilitySettings(context) }) { Text("Open") }
|
GsButton(onClick = { PhoneApps.openAccessibilitySettings(context) }) { Text(stringResource(R.string.guide_open)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import app.seb3thehacker.gearslip.car.theme.*
|
import app.seb3thehacker.gearslip.car.theme.*
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -138,11 +140,11 @@ fun PhoneDialerScreen() {
|
|||||||
|
|
||||||
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
Column(Modifier.fillMaxSize().padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
// The tabs share the title's row, so the list and keypad get the height back.
|
// The tabs share the title's row, so the list and keypad get the height back.
|
||||||
ScreenHeader("Phone") {
|
ScreenHeader(stringResource(R.string.carset_phone)) {
|
||||||
if (hasPermissions) {
|
if (hasPermissions) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
DialerTab("Contacts", tab == TAB_CONTACTS, Modifier.width(140.dp)) { tab = TAB_CONTACTS }
|
DialerTab(stringResource(R.string.dialer_contacts), tab == TAB_CONTACTS, Modifier.width(140.dp)) { tab = TAB_CONTACTS }
|
||||||
DialerTab("Keypad", tab == TAB_KEYPAD, Modifier.width(140.dp)) { tab = TAB_KEYPAD }
|
DialerTab(stringResource(R.string.dialer_keypad), tab == TAB_KEYPAD, Modifier.width(140.dp)) { tab = TAB_KEYPAD }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,14 +170,13 @@ private fun MissingPermission(context: Context, onGranted: () -> Unit) {
|
|||||||
}
|
}
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
Text(
|
Text(
|
||||||
"Gearslip needs the Phone and Contacts permissions to dial from here. Allow them on " +
|
stringResource(R.string.dialer_needs_permission),
|
||||||
"the phone when you're parked.",
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
GsButton(tone = GsTone.Tonal, onClick = {
|
GsButton(tone = GsTone.Tonal, onClick = {
|
||||||
app.seb3thehacker.gearslip.PermissionActivity.ask(context, CALL_PERMISSIONS)
|
app.seb3thehacker.gearslip.PermissionActivity.ask(context, CALL_PERMISSIONS)
|
||||||
}) { Text("Ask on the phone") }
|
}) { Text(stringResource(R.string.dialer_ask)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +212,7 @@ private fun Keypad(context: Context) {
|
|||||||
|
|
||||||
Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
Text(
|
Text(
|
||||||
number.ifEmpty { "Enter a number" },
|
number.ifEmpty { stringResource(R.string.dialer_enter_number) },
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
color = if (number.isEmpty()) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurface,
|
color = if (number.isEmpty()) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurface,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
@@ -240,7 +241,7 @@ private fun Keypad(context: Context) {
|
|||||||
colors = gsColors(GsTone.Primary),
|
colors = gsColors(GsTone.Primary),
|
||||||
enabled = number.isNotEmpty(),
|
enabled = number.isNotEmpty(),
|
||||||
modifier = Modifier.weight(1f).fillMaxWidth(),
|
modifier = Modifier.weight(1f).fillMaxWidth(),
|
||||||
) { Icon(Icons.Filled.Call, "Call") }
|
) { Icon(Icons.Filled.Call, stringResource(R.string.dialer_call)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -282,12 +283,12 @@ private fun ContactsList(context: Context) {
|
|||||||
Box(Modifier.weight(1f).fillMaxWidth()) {
|
Box(Modifier.weight(1f).fillMaxWidth()) {
|
||||||
when {
|
when {
|
||||||
contacts.isEmpty() -> Text(
|
contacts.isEmpty() -> Text(
|
||||||
"No contacts with a phone number found.",
|
stringResource(R.string.dialer_no_contacts),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
filtered.isEmpty() -> Text(
|
filtered.isEmpty() -> Text(
|
||||||
"No contacts match \"$query\".",
|
stringResource(R.string.dialer_no_match, query),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
@@ -344,14 +345,14 @@ private fun ConfirmCallOverlay(contact: Contact, onConfirm: () -> Unit, onDismis
|
|||||||
verticalArrangement = Arrangement.spacedBy(20.dp),
|
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
Text("Call ${contact.name}?", style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
Text(stringResource(R.string.dialer_call_who, contact.name), style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
|
||||||
Text(contact.number, style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Text(contact.number, style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
GsButton(onClick = onDismiss, tone = GsTone.Outline, modifier = Modifier.weight(1f).height(56.dp)) {
|
GsButton(onClick = onDismiss, tone = GsTone.Outline, modifier = Modifier.weight(1f).height(56.dp)) {
|
||||||
Text("Cancel", style = MaterialTheme.typography.titleMedium)
|
Text(stringResource(R.string.cancel), style = MaterialTheme.typography.titleMedium)
|
||||||
}
|
}
|
||||||
GsButton(onClick = onConfirm, modifier = Modifier.weight(1f).height(56.dp)) {
|
GsButton(onClick = onConfirm, modifier = Modifier.weight(1f).height(56.dp)) {
|
||||||
Text("Call", style = MaterialTheme.typography.titleMedium)
|
Text(stringResource(R.string.dialer_call), style = MaterialTheme.typography.titleMedium)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -368,7 +369,7 @@ private fun ContactSearchField(query: String, active: Boolean, onToggle: () -> U
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
query.ifEmpty { "Search contacts" },
|
query.ifEmpty { stringResource(R.string.dialer_search) },
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
color = if (query.isEmpty()) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurface,
|
color = if (query.isEmpty()) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.onSurface,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
@@ -406,7 +407,7 @@ private fun ContactRow(contact: Contact, onClick: () -> Unit) {
|
|||||||
Text(contact.name, style = MaterialTheme.typography.titleMedium, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
Text(contact.name, style = MaterialTheme.typography.titleMedium, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||||
Text(contact.number, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
Text(contact.number, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
}
|
}
|
||||||
Icon(Icons.Filled.Call, contentDescription = "Call", tint = MaterialTheme.colorScheme.primary)
|
Icon(Icons.Filled.Call, contentDescription = stringResource(R.string.dialer_call), tint = MaterialTheme.colorScheme.primary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import android.media.session.PlaybackState
|
import android.media.session.PlaybackState
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -100,17 +102,17 @@ internal fun TransportRow(now: NowPlaying, media: CarMedia, skip: Dp, modifier:
|
|||||||
val play = skip * 1.25f
|
val play = skip * 1.25f
|
||||||
Row(modifier, horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically) {
|
Row(modifier, horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically) {
|
||||||
GsIconButton(
|
GsIconButton(
|
||||||
MediaIcons.Previous, "Previous", media::previous,
|
MediaIcons.Previous, stringResource(R.string.player_previous), media::previous,
|
||||||
enabled = now.canDo(PlaybackState.ACTION_SKIP_TO_PREVIOUS), size = skip, iconSize = skip * 0.58f, shape = CircleShape,
|
enabled = now.canDo(PlaybackState.ACTION_SKIP_TO_PREVIOUS), size = skip, iconSize = skip * 0.58f, shape = CircleShape,
|
||||||
)
|
)
|
||||||
GsIconButton(
|
GsIconButton(
|
||||||
if (now.playing) MediaIcons.Pause else Icons.Filled.PlayArrow,
|
if (now.playing) MediaIcons.Pause else Icons.Filled.PlayArrow,
|
||||||
if (now.playing) "Pause" else "Play",
|
stringResource(if (now.playing) R.string.player_pause else R.string.media_play),
|
||||||
media::togglePlay,
|
media::togglePlay,
|
||||||
tone = GsTone.Primary, size = play, iconSize = play * 0.58f, shape = CircleShape,
|
tone = GsTone.Primary, size = play, iconSize = play * 0.58f, shape = CircleShape,
|
||||||
)
|
)
|
||||||
GsIconButton(
|
GsIconButton(
|
||||||
MediaIcons.Next, "Next", media::next,
|
MediaIcons.Next, stringResource(R.string.player_next), media::next,
|
||||||
enabled = now.canDo(PlaybackState.ACTION_SKIP_TO_NEXT), size = skip, iconSize = skip * 0.58f, shape = CircleShape,
|
enabled = now.canDo(PlaybackState.ACTION_SKIP_TO_NEXT), size = skip, iconSize = skip * 0.58f, shape = CircleShape,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -147,7 +149,7 @@ internal fun PlayModeRow(
|
|||||||
} else {
|
} else {
|
||||||
GsIconButton(
|
GsIconButton(
|
||||||
if (liked == true) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder,
|
if (liked == true) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder,
|
||||||
if (liked == true) "Unlike" else "Like",
|
stringResource(if (liked == true) R.string.player_unlike else R.string.player_like),
|
||||||
media::toggleLike,
|
media::toggleLike,
|
||||||
enabled = liked != null,
|
enabled = liked != null,
|
||||||
tone = if (liked == true) GsTone.Primary else GsTone.Neutral,
|
tone = if (liked == true) GsTone.Primary else GsTone.Neutral,
|
||||||
@@ -159,7 +161,7 @@ internal fun PlayModeRow(
|
|||||||
CustomActionButton(shuffleAction, media, size, toggle = true)
|
CustomActionButton(shuffleAction, media, size, toggle = true)
|
||||||
} else {
|
} else {
|
||||||
GsIconButton(
|
GsIconButton(
|
||||||
MediaIcons.Shuffle, if (shuffleOn) "Shuffle off" else "Shuffle on",
|
MediaIcons.Shuffle, stringResource(if (shuffleOn) R.string.player_shuffle_off else R.string.player_shuffle_on),
|
||||||
media::toggleShuffle,
|
media::toggleShuffle,
|
||||||
enabled = shuffleKnown,
|
enabled = shuffleKnown,
|
||||||
tone = if (shuffleOn) GsTone.Primary else GsTone.Neutral,
|
tone = if (shuffleOn) GsTone.Primary else GsTone.Neutral,
|
||||||
@@ -172,7 +174,7 @@ internal fun PlayModeRow(
|
|||||||
} else {
|
} else {
|
||||||
GsIconButton(
|
GsIconButton(
|
||||||
if (modes.repeat == PlaybackStateCompat.REPEAT_MODE_ONE) MediaIcons.RepeatOne else MediaIcons.Repeat,
|
if (modes.repeat == PlaybackStateCompat.REPEAT_MODE_ONE) MediaIcons.RepeatOne else MediaIcons.Repeat,
|
||||||
"Repeat",
|
stringResource(R.string.player_repeat),
|
||||||
media::cycleRepeat,
|
media::cycleRepeat,
|
||||||
enabled = repeatKnown,
|
enabled = repeatKnown,
|
||||||
tone = if (repeatOn) GsTone.Primary else GsTone.Neutral,
|
tone = if (repeatOn) GsTone.Primary else GsTone.Neutral,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
@@ -29,7 +31,7 @@ fun ScreenHeader(title: String, modifier: Modifier = Modifier, trailing: @Compos
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
GsIconButton(Icons.AutoMirrored.Filled.ArrowBack, "Back", navigator::back)
|
GsIconButton(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.back), navigator::back)
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import app.seb3thehacker.gearslip.car.theme.*
|
import app.seb3thehacker.gearslip.car.theme.*
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
@@ -257,7 +259,7 @@ private fun NavigationBars(
|
|||||||
private fun RoutingCard(info: RoutingInfo, modifier: Modifier = Modifier) {
|
private fun RoutingCard(info: RoutingInfo, modifier: Modifier = Modifier) {
|
||||||
if (info.isLoading) {
|
if (info.isLoading) {
|
||||||
ChromeSurface(modifier) {
|
ChromeSurface(modifier) {
|
||||||
Text("Finding a route…", Modifier.padding(horizontal = 18.dp, vertical = 16.dp), style = ChromeType.title)
|
Text(stringResource(R.string.nav_finding_route), Modifier.padding(horizontal = 18.dp, vertical = 16.dp), style = ChromeType.title)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -312,7 +314,7 @@ private fun RoutingCard(info: RoutingInfo, modifier: Modifier = Modifier) {
|
|||||||
Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 10.dp),
|
Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 10.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text("Then", style = ChromeType.label, color = scheme.onSurfaceVariant)
|
Text(stringResource(R.string.nav_then), style = ChromeType.label, color = scheme.onSurfaceVariant)
|
||||||
Spacer(Modifier.width(10.dp))
|
Spacer(Modifier.width(10.dp))
|
||||||
CarGlyph(next.maneuver?.icon, Modifier.size(28.dp))
|
CarGlyph(next.maneuver?.icon, Modifier.size(28.dp))
|
||||||
val nextText = next.cue.text().ifEmpty { next.road.text() }
|
val nextText = next.cue.text().ifEmpty { next.road.text() }
|
||||||
@@ -374,7 +376,7 @@ private fun LaneTile(lane: Lane) {
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Filled.KeyboardArrowUp,
|
Icons.Filled.KeyboardArrowUp,
|
||||||
contentDescription = if (recommended) "Recommended lane" else "Lane",
|
contentDescription = stringResource(if (recommended) R.string.nav_lane_recommended else R.string.nav_lane),
|
||||||
tint = if (recommended) MaterialTheme.colorScheme.onPrimaryContainer
|
tint = if (recommended) MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
else MaterialTheme.colorScheme.onSurfaceVariant,
|
else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.size(24.dp).rotate(shown.turnDegrees()),
|
modifier = Modifier.size(24.dp).rotate(shown.turnDegrees()),
|
||||||
@@ -427,7 +429,7 @@ private fun TravelEstimateCard(estimate: TravelEstimate, modifier: Modifier = Mo
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
val detail = listOfNotNull(
|
val detail = listOfNotNull(
|
||||||
arrival.takeIf { it.isNotEmpty() }?.let { "Arrive $it" },
|
arrival.takeIf { it.isNotEmpty() }?.let { stringResource(R.string.nav_arrive, it) },
|
||||||
distance.takeIf { it.isNotEmpty() },
|
distance.takeIf { it.isNotEmpty() },
|
||||||
).joinToString(" • ")
|
).joinToString(" • ")
|
||||||
if (detail.isNotEmpty()) {
|
if (detail.isNotEmpty()) {
|
||||||
@@ -442,7 +444,7 @@ private fun TravelEstimateCard(estimate: TravelEstimate, modifier: Modifier = Mo
|
|||||||
// pops the on-screen card, leaving the app still believing it is navigating.
|
// pops the on-screen card, leaving the app still believing it is navigating.
|
||||||
GsIconButton(
|
GsIconButton(
|
||||||
Icons.Filled.Close,
|
Icons.Filled.Close,
|
||||||
"Stop navigating",
|
stringResource(R.string.nav_stop),
|
||||||
{ connection.stopNavigating() },
|
{ connection.stopNavigating() },
|
||||||
colors = GsColors(MaterialTheme.colorScheme.surfaceContainerHighest, MaterialTheme.colorScheme.onSurface),
|
colors = GsColors(MaterialTheme.colorScheme.surfaceContainerHighest, MaterialTheme.colorScheme.onSurface),
|
||||||
size = 52.dp,
|
size = 52.dp,
|
||||||
@@ -575,7 +577,7 @@ private fun RoutePreviewChrome(template: RoutePreviewNavigationTemplate, modifie
|
|||||||
)
|
)
|
||||||
Box(Modifier.weight(1f, fill = false)) {
|
Box(Modifier.weight(1f, fill = false)) {
|
||||||
if (template.isLoading) {
|
if (template.isLoading) {
|
||||||
Text("Finding routes…", Modifier.padding(14.dp), style = ChromeType.title)
|
Text(stringResource(R.string.nav_finding_routes), Modifier.padding(14.dp), style = ChromeType.title)
|
||||||
} else {
|
} else {
|
||||||
ItemListColumn(template.itemList)
|
ItemListColumn(template.itemList)
|
||||||
}
|
}
|
||||||
@@ -599,7 +601,7 @@ private fun ListPane(
|
|||||||
ChromeSurface(modifier, shape = androidx.compose.foundation.shape.RoundedCornerShape(0.dp)) {
|
ChromeSurface(modifier, shape = androidx.compose.foundation.shape.RoundedCornerShape(0.dp)) {
|
||||||
Column {
|
Column {
|
||||||
HeaderBar(title, startAction)
|
HeaderBar(title, startAction)
|
||||||
if (loading) Text("Loading…", Modifier.padding(14.dp), style = ChromeType.title)
|
if (loading) Text(stringResource(R.string.media_loading), Modifier.padding(14.dp), style = ChromeType.title)
|
||||||
else ItemListColumn(itemList)
|
else ItemListColumn(itemList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import app.seb3thehacker.gearslip.car.theme.*
|
import app.seb3thehacker.gearslip.car.theme.*
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
@@ -317,7 +319,7 @@ internal fun SearchField(
|
|||||||
Spacer(Modifier.width(12.dp))
|
Spacer(Modifier.width(12.dp))
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
query.ifEmpty { hint.ifEmpty { "Search" } },
|
query.ifEmpty { hint.ifEmpty { stringResource(R.string.template_search) } },
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
color = if (query.isEmpty()) muted else MaterialTheme.colorScheme.onSurface,
|
color = if (query.isEmpty()) muted else MaterialTheme.colorScheme.onSurface,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
@@ -327,7 +329,7 @@ internal fun SearchField(
|
|||||||
if (onClear != null && query.isNotEmpty()) {
|
if (onClear != null && query.isNotEmpty()) {
|
||||||
GsIconButton(
|
GsIconButton(
|
||||||
androidx.compose.material.icons.Icons.Filled.Close,
|
androidx.compose.material.icons.Icons.Filled.Close,
|
||||||
"Clear",
|
stringResource(R.string.storage_clear),
|
||||||
onClear,
|
onClear,
|
||||||
colors = GsColors(androidx.compose.ui.graphics.Color.Transparent, muted),
|
colors = GsColors(androidx.compose.ui.graphics.Color.Transparent, muted),
|
||||||
size = 42.dp,
|
size = 42.dp,
|
||||||
@@ -623,7 +625,7 @@ private fun MediaPlaybackContent(template: MediaPlaybackTemplate, modifier: Modi
|
|||||||
} else {
|
} else {
|
||||||
Box(Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) {
|
Box(Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||||
Text(
|
Text(
|
||||||
"Nothing playing yet.",
|
stringResource(R.string.template_nothing_playing),
|
||||||
style = ChromeType.body,
|
style = ChromeType.body,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
|
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
|
||||||
@@ -647,6 +649,6 @@ private typealias ColumnScopeAlias = androidx.compose.foundation.layout.ColumnSc
|
|||||||
@Composable
|
@Composable
|
||||||
private fun Loading() {
|
private fun Loading() {
|
||||||
Box(Modifier.fillMaxWidth().height(80.dp), contentAlignment = Alignment.Center) {
|
Box(Modifier.fillMaxWidth().height(80.dp), contentAlignment = Alignment.Center) {
|
||||||
Text("Loading…", style = ChromeType.title)
|
Text(stringResource(R.string.media_loading), style = ChromeType.title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import app.seb3thehacker.gearslip.car.theme.*
|
import app.seb3thehacker.gearslip.car.theme.*
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -425,7 +427,7 @@ internal fun AutoStartButton(action: Action, modifier: Modifier = Modifier) {
|
|||||||
) {
|
) {
|
||||||
if (hasGlyph) CarGlyph(action.icon, Modifier.size(24.dp).align(Alignment.CenterStart))
|
if (hasGlyph) CarGlyph(action.icon, Modifier.size(24.dp).align(Alignment.CenterStart))
|
||||||
Text(
|
Text(
|
||||||
action.title.text().ifEmpty { "Start" },
|
action.title.text().ifEmpty { stringResource(R.string.template_start) },
|
||||||
style = ChromeType.title,
|
style = ChromeType.title,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import app.seb3thehacker.gearslip.car.theme.*
|
import app.seb3thehacker.gearslip.car.theme.*
|
||||||
import androidx.compose.runtime.produceState
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -300,7 +302,7 @@ private fun DetailRow(row: CarRow, placeName: String) {
|
|||||||
Row(Modifier.padding(horizontal = 16.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically) {
|
Row(Modifier.padding(horizontal = 16.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
androidx.compose.material3.Icon(Icons.Filled.Call, null, Modifier.size(20.dp))
|
androidx.compose.material3.Icon(Icons.Filled.Call, null, Modifier.size(20.dp))
|
||||||
Spacer(Modifier.width(6.dp))
|
Spacer(Modifier.width(6.dp))
|
||||||
Text("Call", style = ChromeType.body)
|
Text(stringResource(R.string.dialer_call), style = ChromeType.body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
@@ -38,7 +42,7 @@ class WeatherData(
|
|||||||
|
|
||||||
sealed interface WeatherState {
|
sealed interface WeatherState {
|
||||||
data object Loading : WeatherState
|
data object Loading : WeatherState
|
||||||
data class Unavailable(val reason: String) : WeatherState
|
data class Unavailable(@param:StringRes val reason: Int) : WeatherState
|
||||||
data class Ready(val data: WeatherData) : WeatherState
|
data class Ready(val data: WeatherData) : WeatherState
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +82,7 @@ object Weather {
|
|||||||
if (flow.value !is WeatherState.Ready) {
|
if (flow.value !is WeatherState.Ready) {
|
||||||
val allowed = hasLocationPermission(context)
|
val allowed = hasLocationPermission(context)
|
||||||
flow.value = WeatherState.Unavailable(
|
flow.value = WeatherState.Unavailable(
|
||||||
if (allowed) "Waiting for a location fix." else "Allow location for Gearslip on the phone to see the weather.",
|
if (allowed) R.string.weather_waiting_fix else R.string.weather_allow_location,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -92,7 +96,7 @@ object Weather {
|
|||||||
// No place name: logs get shared in bug reports.
|
// No place name: logs get shared in bug reports.
|
||||||
GearslipLog.i("weather: loaded")
|
GearslipLog.i("weather: loaded")
|
||||||
}
|
}
|
||||||
else if (flow.value !is WeatherState.Ready) flow.value = WeatherState.Unavailable("Could not reach the weather service.")
|
else if (flow.value !is WeatherState.Ready) flow.value = WeatherState.Unavailable(R.string.weather_unreachable)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- location -------------------------------------------------------------------------
|
// --- location -------------------------------------------------------------------------
|
||||||
@@ -197,6 +201,26 @@ object Weather {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Open-Meteo's WMO weather codes, in words on the screen. [describe] is the English the assistant says. */
|
||||||
|
@StringRes
|
||||||
|
fun conditionRes(code: Int): Int = when (code) {
|
||||||
|
0 -> R.string.weather_clear
|
||||||
|
1 -> R.string.weather_mostly_clear
|
||||||
|
2 -> R.string.weather_partly_cloudy
|
||||||
|
3 -> R.string.weather_overcast
|
||||||
|
45, 48 -> R.string.weather_fog
|
||||||
|
in 51..57 -> R.string.weather_drizzle
|
||||||
|
in 61..67 -> R.string.weather_rain
|
||||||
|
in 71..77 -> R.string.weather_snow
|
||||||
|
in 80..82 -> R.string.weather_rain_showers
|
||||||
|
in 85..86 -> R.string.weather_snow_showers
|
||||||
|
in 95..99 -> R.string.weather_thunderstorm
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun condition(code: Int): String = conditionRes(code).let { if (it == 0) "-" else stringResource(it) }
|
||||||
|
|
||||||
/** Open-Meteo's WMO weather codes, in words. */
|
/** Open-Meteo's WMO weather codes, in words. */
|
||||||
fun describe(code: Int): String = when (code) {
|
fun describe(code: Int): String = when (code) {
|
||||||
0 -> "Clear"
|
0 -> "Clear"
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -41,11 +43,11 @@ fun WeatherScreen() {
|
|||||||
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 24.dp, vertical = 16.dp),
|
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 24.dp, vertical = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(20.dp),
|
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||||
) {
|
) {
|
||||||
ScreenHeader("Weather")
|
ScreenHeader(stringResource(R.string.crumb_weather))
|
||||||
when (val s = state) {
|
when (val s = state) {
|
||||||
WeatherState.Loading -> Text("Getting the weather…", style = MaterialTheme.typography.titleMedium)
|
WeatherState.Loading -> Text(stringResource(R.string.weather_getting), style = MaterialTheme.typography.titleMedium)
|
||||||
is WeatherState.Unavailable -> Text(
|
is WeatherState.Unavailable -> Text(
|
||||||
s.reason, style = MaterialTheme.typography.titleMedium,
|
stringResource(s.reason), style = MaterialTheme.typography.titleMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
is WeatherState.Ready -> Forecast(s.data)
|
is WeatherState.Ready -> Forecast(s.data)
|
||||||
@@ -66,18 +68,18 @@ private fun Forecast(w: WeatherData) {
|
|||||||
WeatherIcon(w.code, w.isDay, size = 88.dp)
|
WeatherIcon(w.code, w.isDay, size = 88.dp)
|
||||||
Column {
|
Column {
|
||||||
Text("${w.temp}${w.tempUnit}", fontSize = 56.sp, fontWeight = FontWeight.Bold)
|
Text("${w.temp}${w.tempUnit}", fontSize = 56.sp, fontWeight = FontWeight.Bold)
|
||||||
Text(Weather.describe(w.code), style = MaterialTheme.typography.titleLarge)
|
Text(Weather.condition(w.code), style = MaterialTheme.typography.titleLarge)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
Column(horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
Column(horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
Text("Feels like ${w.feelsLike}${w.tempUnit}", style = MaterialTheme.typography.bodyLarge)
|
Text(stringResource(R.string.weather_feels_like, "${w.feelsLike}${w.tempUnit}"), style = MaterialTheme.typography.bodyLarge)
|
||||||
Text("Humidity ${w.humidity}%", style = MaterialTheme.typography.bodyLarge)
|
Text(stringResource(R.string.weather_humidity, w.humidity), style = MaterialTheme.typography.bodyLarge)
|
||||||
Text("Wind ${w.wind} ${w.windUnit}", style = MaterialTheme.typography.bodyLarge)
|
Text(stringResource(R.string.weather_wind, "${w.wind} ${w.windUnit}"), style = MaterialTheme.typography.bodyLarge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text("Next hours", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary)
|
Text(stringResource(R.string.weather_next_hours), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary)
|
||||||
LazyRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
LazyRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||||
items(w.hours) { h ->
|
items(w.hours) { h ->
|
||||||
Surface(color = MaterialTheme.colorScheme.surfaceContainerHigh, shape = MaterialTheme.shapes.large) {
|
Surface(color = MaterialTheme.colorScheme.surfaceContainerHigh, shape = MaterialTheme.shapes.large) {
|
||||||
@@ -101,7 +103,7 @@ private fun Forecast(w: WeatherData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text("This week", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary)
|
Text(stringResource(R.string.weather_this_week), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.primary)
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
w.days.forEachIndexed { i, d ->
|
w.days.forEachIndexed { i, d ->
|
||||||
Surface(color = MaterialTheme.colorScheme.surfaceContainerHigh, shape = MaterialTheme.shapes.large) {
|
Surface(color = MaterialTheme.colorScheme.surfaceContainerHigh, shape = MaterialTheme.shapes.large) {
|
||||||
@@ -111,11 +113,11 @@ private fun Forecast(w: WeatherData) {
|
|||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
if (i == 0) "Today" else SimpleDateFormat("EEEE", Locale.getDefault()).format(Date(d.date)),
|
if (i == 0) stringResource(R.string.weather_today) else SimpleDateFormat("EEEE", Locale.getDefault()).format(Date(d.date)),
|
||||||
Modifier.width(120.dp), style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.SemiBold,
|
Modifier.width(120.dp), style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.SemiBold,
|
||||||
)
|
)
|
||||||
WeatherIcon(d.code, true, size = 28.dp)
|
WeatherIcon(d.code, true, size = 28.dp)
|
||||||
Text(Weather.describe(d.code), Modifier.weight(1f), style = MaterialTheme.typography.bodyMedium)
|
Text(Weather.condition(d.code), Modifier.weight(1f), style = MaterialTheme.typography.bodyMedium)
|
||||||
if (d.rainChance > 0) Text("${d.rainChance}%", color = MaterialTheme.colorScheme.primary)
|
if (d.rainChance > 0) Text("${d.rainChance}%", color = MaterialTheme.colorScheme.primary)
|
||||||
Text("${d.low}° / ${d.high}°", style = MaterialTheme.typography.bodyLarge)
|
Text("${d.low}° / ${d.high}°", style = MaterialTheme.typography.bodyLarge)
|
||||||
}
|
}
|
||||||
@@ -123,7 +125,7 @@ private fun Forecast(w: WeatherData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
"Weather data by Open-Meteo.com (CC BY 4.0)",
|
stringResource(R.string.weather_credit),
|
||||||
style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant,
|
style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.seb3thehacker.gearslip.car
|
package app.seb3thehacker.gearslip.car
|
||||||
|
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import app.seb3thehacker.gearslip.R
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -95,19 +97,19 @@ fun WebApp(content: String) {
|
|||||||
Column(Modifier.fillMaxSize()) {
|
Column(Modifier.fillMaxSize()) {
|
||||||
// Back steps through the page's history first (the interceptor above), then leaves.
|
// Back steps through the page's history first (the interceptor above), then leaves.
|
||||||
if (!fullscreen) {
|
if (!fullscreen) {
|
||||||
ScreenHeader(pageTitle.ifEmpty { "Browser" }, Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
|
ScreenHeader(pageTitle.ifEmpty { stringResource(R.string.app_browser) }, Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) {
|
||||||
GsIconButton(androidx.compose.material.icons.Icons.Filled.Search, "Search or type an address", {
|
GsIconButton(androidx.compose.material.icons.Icons.Filled.Search, stringResource(R.string.web_search), {
|
||||||
fieldText = null
|
fieldText = null
|
||||||
address = ""
|
address = ""
|
||||||
})
|
})
|
||||||
if (canGoForward) {
|
if (canGoForward) {
|
||||||
GsIconButton(androidx.compose.material.icons.Icons.AutoMirrored.Filled.ArrowForward, "Forward", { web?.goForward() })
|
GsIconButton(androidx.compose.material.icons.Icons.AutoMirrored.Filled.ArrowForward, stringResource(R.string.web_forward), { web?.goForward() })
|
||||||
}
|
}
|
||||||
GsIconButton(androidx.compose.material.icons.Icons.Filled.Refresh, "Reload", { web?.reload() })
|
GsIconButton(androidx.compose.material.icons.Icons.Filled.Refresh, stringResource(R.string.web_reload), { web?.reload() })
|
||||||
GsIconButton(androidx.compose.material.icons.Icons.Filled.Home, "Start page", {
|
GsIconButton(androidx.compose.material.icons.Icons.Filled.Home, stringResource(R.string.web_start_page), {
|
||||||
web?.let { view -> loadContent(view, content) }
|
web?.let { view -> loadContent(view, content) }
|
||||||
})
|
})
|
||||||
GsIconButton(MediaIcons.Fullscreen, "Full screen", CarFullscreen::enter)
|
GsIconButton(MediaIcons.Fullscreen, stringResource(R.string.web_fullscreen), CarFullscreen::enter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Box(Modifier.weight(1f).fillMaxSize()) {
|
Box(Modifier.weight(1f).fillMaxSize()) {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ private fun navBarChoices() = listOf(
|
|||||||
NavBarMode.HIDE to stringResource(R.string.choice_nav_hide),
|
NavBarMode.HIDE to stringResource(R.string.choice_nav_hide),
|
||||||
)
|
)
|
||||||
@Composable
|
@Composable
|
||||||
private fun openChoices() = listOf(CarSettings.HOME to stringResource(R.string.choice_open_home)) + CarApps.all.filter { it.id != "settings" }.map { it.id to it.label }
|
private fun openChoices() = listOf(CarSettings.HOME to stringResource(R.string.choice_open_home)) + CarApps.all.filter { it.id != "settings" }.map { it.id to stringResource(it.labelRes) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The car's settings on the phone's home screen: one card per group with a summary of where it
|
* The car's settings on the phone's home screen: one card per group with a summary of where it
|
||||||
|
|||||||
@@ -432,4 +432,412 @@
|
|||||||
|
|
||||||
<!-- -->
|
<!-- -->
|
||||||
<string name="whats_new_this_version">%1$s (this version)</string>
|
<string name="whats_new_this_version">%1$s (this version)</string>
|
||||||
|
|
||||||
|
<!-- Car settings screen -->
|
||||||
|
<string name="carset_theme">Theme</string>
|
||||||
|
<string name="carset_imperial">Imperial (mi, ft, °F)</string>
|
||||||
|
<string name="carset_metric">Metric (km, m, °C)</string>
|
||||||
|
<string name="carset_screen_size">Screen size (next connection)</string>
|
||||||
|
<string name="carset_fit">Fit to the screen</string>
|
||||||
|
<string name="off">Off</string>
|
||||||
|
<string name="on">On</string>
|
||||||
|
<string name="carset_lyrics_from">Lyrics from</string>
|
||||||
|
<string name="carset_more_sources">More sources</string>
|
||||||
|
<string name="carset_hide">Hide</string>
|
||||||
|
<string name="carset_show">Show</string>
|
||||||
|
<string name="carset_phone">Phone</string>
|
||||||
|
<string name="carset_vehicle">Vehicle</string>
|
||||||
|
<string name="carset_video">Video</string>
|
||||||
|
<string name="carset_version">Version</string>
|
||||||
|
<string name="carset_build">Build</string>
|
||||||
|
<string name="carset_whats_new_after">What\'s new after updates</string>
|
||||||
|
<string name="carset_whats_new_after_sub">Show the list of changes once, the first time the car connects after an update.</string>
|
||||||
|
<string name="carset_web_default">DuckDuckGo (default) - tap to set a different page</string>
|
||||||
|
<string name="carset_web_note">Shown by the Browser app on the launcher. Applies on the next connection.</string>
|
||||||
|
<string name="carset_screen_fit">Screen fit</string>
|
||||||
|
<string name="carset_top">Top</string>
|
||||||
|
<string name="carset_bottom">Bottom</string>
|
||||||
|
<string name="carset_save_vehicle">Save for this vehicle</string>
|
||||||
|
<string name="carset_no_profile">No vehicle profile matched</string>
|
||||||
|
|
||||||
|
<!-- -->
|
||||||
|
<string name="carset_audio_note">USB sends the phone\'s sound through Gearslip. Android asks to share your screen each drive, and a few apps block it. Bluetooth plays as the phone normally would, and Android never asks.</string>
|
||||||
|
<string name="carset_directions_note">Gearslip reads each turn aloud through the car\'s speakers, and the music dips while it speaks. Turn it off if your map app\'s own voice already plays in the car.</string>
|
||||||
|
<string name="carset_phone_speech_note">Listens through the phone\'s own speech service instead of Gearslip\'s. On most phones that\'s Google\'s, which may send your voice to Google. GrapheneOS has none, so Gearslip keeps using its own.</string>
|
||||||
|
<string name="carset_vehicle_data_note">Adds a Vehicle data tile: a live, raw dump of the head unit\'s sensor channel. Field labels (speed, RPM, fuel...) are carried over from other reverse-engineering projects and unverified against this car - treat them as a guess, not a fact, until you\'ve watched one change for yourself.</string>
|
||||||
|
<string name="carset_adaptive_note">Lowers the picture\'s quality when the car, or its Wi-Fi, can\'t keep up, and raises it again once it can. Takes effect on the next connection.</string>
|
||||||
|
<string name="carset_usable_height">Usable height %1$dpx of 480</string>
|
||||||
|
<string name="carset_saved_to">Saved to %1$s</string>
|
||||||
|
<string name="carset_save_failed">Could not save</string>
|
||||||
|
<string name="carset_px">%1$dpx</string>
|
||||||
|
|
||||||
|
<!-- Car media app -->
|
||||||
|
<string name="media_open_on_phone">Open on phone</string>
|
||||||
|
<string name="media_now_playing">Now playing</string>
|
||||||
|
<string name="media_up_next">Up next</string>
|
||||||
|
<string name="media_browse">Browse</string>
|
||||||
|
<string name="media_search_hint">Type a song, artist, album, or playlist.</string>
|
||||||
|
<string name="media_looking_lyrics">Looking for lyrics…</string>
|
||||||
|
<string name="media_nothing_playing">Nothing playing</string>
|
||||||
|
<string name="media_press_play">Press play to pick up where you left off, or find something in Browse.</string>
|
||||||
|
|
||||||
|
<!-- -->
|
||||||
|
<string name="media_keeps_library">%1$s keeps its library to itself.</string>
|
||||||
|
<string name="media_needs_access">Gearslip can still control it once it has Notification access on the phone. Allow it under Settings > Notifications > Notification access.</string>
|
||||||
|
<string name="media_not_running">%1$s isn\'t running.</string>
|
||||||
|
<string name="media_not_running_body">%1$s plays through Gearslip\'s own player, but only while it\'s open on the phone. Open it once and it keeps playing here in the car.</string>
|
||||||
|
<string name="media_open_app">Open %1$s</string>
|
||||||
|
<string name="media_refused">%1$s would not let Gearslip browse it.</string>
|
||||||
|
<string name="media_refused_body">Some media apps only accept Google\'s own host.</string>
|
||||||
|
<string name="media_connecting">Connecting to %1$s…</string>
|
||||||
|
<string name="media_search">Search %1$s</string>
|
||||||
|
<string name="media_search_no_list">%1$s can\'t list results. Press search, and %1$s picks what to play.</string>
|
||||||
|
<string name="media_search_unsupported">%1$s can\'t search from the car.</string>
|
||||||
|
<string name="media_searching">Searching %1$s…</string>
|
||||||
|
<string name="media_search_nothing">%1$s found nothing for \"%2$s\".</string>
|
||||||
|
<string name="media_search_ignored">%1$s didn\'t play anything for \"%2$s\".</string>
|
||||||
|
<string name="media_searching_for">Searching %1$s for \"%2$s\"…</string>
|
||||||
|
<string name="media_nothing_playing_dot">Nothing playing.</string>
|
||||||
|
<string name="media_no_lyrics">No lyrics found for this track.</string>
|
||||||
|
|
||||||
|
<!-- Car media browsing -->
|
||||||
|
<string name="media_up">Up</string>
|
||||||
|
<string name="media_loading">Loading…</string>
|
||||||
|
<string name="media_browse_start">Start music in the app on your phone. Play, pause, skip and seek work from here.</string>
|
||||||
|
<string name="media_load_failed">Could not load this list.</string>
|
||||||
|
<string name="media_nothing_here">Nothing here.</string>
|
||||||
|
<string name="media_nothing_queued">Nothing queued.</string>
|
||||||
|
<string name="media_play">Play</string>
|
||||||
|
|
||||||
|
<!-- -->
|
||||||
|
<string name="media_queue_empty_idle">Nothing playing - not every app tells Gearslip what comes next.</string>
|
||||||
|
<string name="media_queue_empty_playing">Only what\'s already playing - not every app tells Gearslip what comes next.</string>
|
||||||
|
|
||||||
|
<!-- Player controls -->
|
||||||
|
<string name="player_previous">Previous</string>
|
||||||
|
<string name="player_next">Next</string>
|
||||||
|
<string name="player_repeat">Repeat</string>
|
||||||
|
|
||||||
|
<!-- -->
|
||||||
|
<string name="player_pause">Pause</string>
|
||||||
|
<string name="player_unlike">Unlike</string>
|
||||||
|
<string name="player_like">Like</string>
|
||||||
|
<string name="player_shuffle_off">Shuffle off</string>
|
||||||
|
<string name="player_shuffle_on">Shuffle on</string>
|
||||||
|
|
||||||
|
<!-- Car home -->
|
||||||
|
<string name="home_choose_app">Choose another app</string>
|
||||||
|
<string name="home_no_maps">No mapping apps found.</string>
|
||||||
|
<string name="home_no_maps_body">Install a navigation app that supports Android Auto and it will start here on its own.</string>
|
||||||
|
<string name="home_map_stopped">Map stopped</string>
|
||||||
|
<string name="home_map_stopped_body">It stays closed until you open it again.</string>
|
||||||
|
<string name="home_reopen_map">Reopen the map</string>
|
||||||
|
<string name="home_car_apps">Car apps</string>
|
||||||
|
<string name="home_back_nav">Back to the nav bar</string>
|
||||||
|
<string name="home_show_player">Show the player</string>
|
||||||
|
<string name="home_hide_lyrics">Hide lyrics</string>
|
||||||
|
|
||||||
|
<!-- -->
|
||||||
|
<string name="home_starting">Starting %1$s…</string>
|
||||||
|
<string name="home_starting_map">Starting the map…</string>
|
||||||
|
<string name="home_failed">%1$s would not start.</string>
|
||||||
|
<string name="home_failed_map">The map app would not start.</string>
|
||||||
|
|
||||||
|
<!-- Breadcrumbs -->
|
||||||
|
<string name="crumb_home">Home</string>
|
||||||
|
<string name="crumb_media">Media</string>
|
||||||
|
<string name="crumb_apps">Apps</string>
|
||||||
|
<string name="crumb_dashboard">Dashboard</string>
|
||||||
|
<string name="crumb_weather">Weather</string>
|
||||||
|
<string name="crumb_reply">Reply</string>
|
||||||
|
|
||||||
|
<!-- Built-in apps -->
|
||||||
|
<string name="app_browser">Browser</string>
|
||||||
|
<string name="app_screen_sharing">Screen sharing</string>
|
||||||
|
<string name="app_exit">Exit</string>
|
||||||
|
<string name="navapps_unpin">Unpin from nav bar</string>
|
||||||
|
<string name="navapps_full">Nav bar is full (%1$d pinned)</string>
|
||||||
|
<string name="navapps_pin">Pin to nav bar</string>
|
||||||
|
<string name="navapps_close">Close app</string>
|
||||||
|
<string name="navapps_move_left">Move left</string>
|
||||||
|
<string name="navapps_move_right">Move right</string>
|
||||||
|
<string name="navapps_reset">Reset app order</string>
|
||||||
|
|
||||||
|
<!-- Message popup -->
|
||||||
|
<string name="msg_dismiss">Dismiss</string>
|
||||||
|
<string name="msg_read">Read</string>
|
||||||
|
<string name="msg_stop">Stop</string>
|
||||||
|
<string name="msg_no">No</string>
|
||||||
|
<string name="msg_send_now">Send now</string>
|
||||||
|
<string name="msg_type">Type</string>
|
||||||
|
|
||||||
|
<!-- -->
|
||||||
|
<plurals name="msg_burst">
|
||||||
|
<item quantity="one">%1$s · %2$d new</item>
|
||||||
|
<item quantity="other">%1$s · %2$d new</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="msg_reply_to">Reply to %1$s</string>
|
||||||
|
<string name="msg_listening">Listening</string>
|
||||||
|
<string name="msg_sending_to">Sending to %1$s</string>
|
||||||
|
<string name="msg_sent_to">Sent to %1$s</string>
|
||||||
|
<string name="msg_reply_failed">Couldn\'t reply</string>
|
||||||
|
|
||||||
|
<!-- Bluetooth pairing -->
|
||||||
|
<string name="bt_pair_unfinished">Pairing didn\'t finish. Pair from the car\'s Bluetooth menu instead.</string>
|
||||||
|
<string name="bt_pair_not_started">Pairing didn\'t start. Pair from the car\'s Bluetooth menu instead.</string>
|
||||||
|
<string name="bt_paired">Paired. Calls now play through the car.</string>
|
||||||
|
|
||||||
|
<!-- Car overlays -->
|
||||||
|
<string name="overlay_dont_show">Don\'t show again</string>
|
||||||
|
<string name="overlay_ok">OK</string>
|
||||||
|
<string name="overlay_location_off">Location is off</string>
|
||||||
|
<string name="overlay_pair_title">Pair with this car?</string>
|
||||||
|
<string name="overlay_pair">Pair</string>
|
||||||
|
<string name="overlay_confirm_title">Confirm on your phone</string>
|
||||||
|
<string name="overlay_confirm_body">Check the code on your phone and tap Pair. The car may ask too.</string>
|
||||||
|
|
||||||
|
<!-- -->
|
||||||
|
<string name="overlay_bt_body">Music plays over Bluetooth, but your phone isn\'t connected to the car. Connect it from the car\'s Bluetooth menu or your phone\'s.</string>
|
||||||
|
<string name="overlay_location_body">Map apps can\'t show where you are or give directions. Turn on location in your phone\'s quick settings when you\'re parked.</string>
|
||||||
|
<string name="overlay_pair_body_music">Calls and music play through the car\'s speakers once your phone and the car are paired. Your phone then asks you to confirm a code.</string>
|
||||||
|
<string name="overlay_pair_body">Calls play through the car\'s speakers once your phone and the car are paired. Your phone then asks you to confirm a code.</string>
|
||||||
|
|
||||||
|
<!-- Weather -->
|
||||||
|
<string name="weather_waiting_fix">Waiting for a location fix.</string>
|
||||||
|
<string name="weather_allow_location">Allow location for Gearslip on the phone to see the weather.</string>
|
||||||
|
<string name="weather_unreachable">Could not reach the weather service.</string>
|
||||||
|
<string name="weather_clear">Clear</string>
|
||||||
|
<string name="weather_mostly_clear">Mostly clear</string>
|
||||||
|
<string name="weather_partly_cloudy">Partly cloudy</string>
|
||||||
|
<string name="weather_overcast">Overcast</string>
|
||||||
|
<string name="weather_fog">Fog</string>
|
||||||
|
<string name="weather_drizzle">Drizzle</string>
|
||||||
|
<string name="weather_rain">Rain</string>
|
||||||
|
<string name="weather_snow">Snow</string>
|
||||||
|
<string name="weather_rain_showers">Rain showers</string>
|
||||||
|
<string name="weather_snow_showers">Snow showers</string>
|
||||||
|
<string name="weather_thunderstorm">Thunderstorm</string>
|
||||||
|
|
||||||
|
<!-- Weather screen -->
|
||||||
|
<string name="weather_getting">Getting the weather…</string>
|
||||||
|
<string name="weather_next_hours">Next hours</string>
|
||||||
|
<string name="weather_this_week">This week</string>
|
||||||
|
<string name="weather_today">Today</string>
|
||||||
|
<string name="weather_credit">Weather data by Open-Meteo.com (CC BY 4.0)</string>
|
||||||
|
|
||||||
|
<!-- -->
|
||||||
|
<string name="weather_feels_like">Feels like %1$s</string>
|
||||||
|
<string name="weather_humidity">Humidity %1$d%%</string>
|
||||||
|
<string name="weather_wind">Wind %1$s</string>
|
||||||
|
|
||||||
|
<!-- Dashboard -->
|
||||||
|
<string name="dash_today">Today</string>
|
||||||
|
<string name="dash_getting_weather">Getting weather…</string>
|
||||||
|
<string name="dash_coming_up">Coming up</string>
|
||||||
|
<string name="dash_loading_calendar">Loading calendar…</string>
|
||||||
|
<string name="dash_nothing_week">Nothing in the next week.</string>
|
||||||
|
|
||||||
|
<!-- -->
|
||||||
|
<string name="dash_all_day">All day</string>
|
||||||
|
|
||||||
|
<!-- Phone dialer -->
|
||||||
|
<string name="dialer_contacts">Contacts</string>
|
||||||
|
<string name="dialer_keypad">Keypad</string>
|
||||||
|
<string name="dialer_ask">Ask on the phone</string>
|
||||||
|
<string name="dialer_enter_number">Enter a number</string>
|
||||||
|
<string name="dialer_call">Call</string>
|
||||||
|
<string name="dialer_no_contacts">No contacts with a phone number found.</string>
|
||||||
|
<string name="dialer_search">Search contacts</string>
|
||||||
|
|
||||||
|
<!-- -->
|
||||||
|
<string name="dialer_needs_permission">Gearslip needs the Phone and Contacts permissions to dial from here. Allow them on the phone when you\'re parked.</string>
|
||||||
|
<string name="dialer_no_match">No contacts match \"%1$s\".</string>
|
||||||
|
<string name="dialer_call_who">Call %1$s?</string>
|
||||||
|
|
||||||
|
<!-- Car screen -->
|
||||||
|
<string name="carui_exit_fullscreen">Exit full screen</string>
|
||||||
|
<string name="carui_drive_safely">Drive safely</string>
|
||||||
|
<string name="carui_understand">I understand</string>
|
||||||
|
|
||||||
|
<!-- -->
|
||||||
|
<string name="carui_unofficial">Gearslip is unofficial software: it is not made or reviewed by any carmaker, and it can fail without warning.</string>
|
||||||
|
<string name="carui_never_interact">Never interact with this screen while driving. Set the route or the song before you go, then keep your attention on the road.</string>
|
||||||
|
<string name="carui_this_app">This app</string>
|
||||||
|
<string name="carui_app_unsupported">%1$s doesn\'t work with Gearslip yet.</string>
|
||||||
|
<string name="carui_app_unsupported_body">It didn\'t start. Some apps only run on Google\'s Android Auto.</string>
|
||||||
|
|
||||||
|
<!-- Nav bar -->
|
||||||
|
<string name="navbar_on_call">On a call</string>
|
||||||
|
<string name="navbar_end_call">End call</string>
|
||||||
|
|
||||||
|
<!-- -->
|
||||||
|
<string name="navbar_unmute">Unmute</string>
|
||||||
|
<string name="navbar_mute">Mute</string>
|
||||||
|
<string name="navbar_close_panel">Close the side panel</string>
|
||||||
|
<string name="navbar_dock_player">Move the player beside the map</string>
|
||||||
|
|
||||||
|
<!-- Browser -->
|
||||||
|
<string name="web_search">Search or type an address</string>
|
||||||
|
<string name="web_forward">Forward</string>
|
||||||
|
<string name="web_reload">Reload</string>
|
||||||
|
<string name="web_start_page">Start page</string>
|
||||||
|
<string name="web_fullscreen">Full screen</string>
|
||||||
|
|
||||||
|
<!-- Notifications -->
|
||||||
|
<string name="notif_title">Notifications</string>
|
||||||
|
<string name="notif_clear_all">Clear all</string>
|
||||||
|
<string name="notif_empty">Nothing yet. New notifications from the phone will show up here.</string>
|
||||||
|
<string name="notif_off">Notifications are off. Allow Gearslip under Settings > Notifications > Notification access on the phone.</string>
|
||||||
|
<string name="notif_replied">Replied</string>
|
||||||
|
|
||||||
|
<!-- Messages -->
|
||||||
|
<string name="msg_mark_read">Mark read</string>
|
||||||
|
|
||||||
|
<!-- -->
|
||||||
|
<string name="msg_messages">Messages</string>
|
||||||
|
<string name="msg_none_recent">No recent messages. New %1$s messages appear here as they arrive on the phone.</string>
|
||||||
|
<plurals name="msg_earlier">
|
||||||
|
<item quantity="one">%1$d earlier</item>
|
||||||
|
<item quantity="other">%1$d earlier</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="msg_hide_quick">Hide quick replies</string>
|
||||||
|
<string name="msg_quick">Quick reply</string>
|
||||||
|
<string name="msg_you">You: </string>
|
||||||
|
<string name="msg_sender">%1$s: </string>
|
||||||
|
|
||||||
|
<!-- Car app navigation -->
|
||||||
|
<string name="nav_finding_route">Finding a route…</string>
|
||||||
|
<string name="nav_then">Then</string>
|
||||||
|
<string name="nav_stop">Stop navigating</string>
|
||||||
|
<string name="nav_finding_routes">Finding routes…</string>
|
||||||
|
|
||||||
|
<!-- -->
|
||||||
|
<string name="nav_lane_recommended">Recommended lane</string>
|
||||||
|
<string name="nav_lane">Lane</string>
|
||||||
|
<string name="nav_arrive">Arrive %1$s</string>
|
||||||
|
|
||||||
|
<!-- Car app screens -->
|
||||||
|
<string name="template_nothing_playing">Nothing playing yet.</string>
|
||||||
|
|
||||||
|
<!-- -->
|
||||||
|
<string name="template_search">Search</string>
|
||||||
|
|
||||||
|
<!-- Car app screens, more -->
|
||||||
|
<string name="template_start">Start</string>
|
||||||
|
|
||||||
|
<!-- Calls -->
|
||||||
|
<string name="call_incoming">Incoming call</string>
|
||||||
|
<string name="call_decline">Decline</string>
|
||||||
|
<string name="call_answer">Answer</string>
|
||||||
|
<string name="call_ringing">Already ringing out loud - answer here, or let it ring.</string>
|
||||||
|
|
||||||
|
<!-- Screen sharing -->
|
||||||
|
<string name="phoneapps_touch_off">Touch is off. Enable Gearslip under Settings > Accessibility on the phone.</string>
|
||||||
|
|
||||||
|
<!-- What's new in the car -->
|
||||||
|
<string name="whatsnew_show_after">Show after updates</string>
|
||||||
|
<string name="whatsnew_got_it">Got it</string>
|
||||||
|
|
||||||
|
<!-- -->
|
||||||
|
<string name="whatsnew_title">What\'s new in %1$s</string>
|
||||||
|
|
||||||
|
<!-- Car keyboard -->
|
||||||
|
<string name="keyboard_space">space</string>
|
||||||
|
<string name="keyboard_hide">Hide keyboard</string>
|
||||||
|
<string name="keyboard_submit">Submit</string>
|
||||||
|
|
||||||
|
<!-- Launcher -->
|
||||||
|
<string name="launcher_broken">Doesn\'t work with Gearslip</string>
|
||||||
|
|
||||||
|
<!-- Car app stage -->
|
||||||
|
<string name="stage_car_app">Car app</string>
|
||||||
|
<string name="stage_close">Close %1$s</string>
|
||||||
|
<string name="stage_close_app">Close the app</string>
|
||||||
|
|
||||||
|
<!-- Spoken directions -->
|
||||||
|
<string name="dir_slight_left">Turn slightly left</string>
|
||||||
|
<string name="dir_slight_left_onto">Turn slightly left onto %1$s</string>
|
||||||
|
<string name="dir_slight_right">Turn slightly right</string>
|
||||||
|
<string name="dir_slight_right_onto">Turn slightly right onto %1$s</string>
|
||||||
|
<string name="dir_left">Turn left</string>
|
||||||
|
<string name="dir_left_onto">Turn left onto %1$s</string>
|
||||||
|
<string name="dir_right">Turn right</string>
|
||||||
|
<string name="dir_right_onto">Turn right onto %1$s</string>
|
||||||
|
<string name="dir_sharp_left">Turn sharply left</string>
|
||||||
|
<string name="dir_sharp_left_onto">Turn sharply left onto %1$s</string>
|
||||||
|
<string name="dir_sharp_right">Turn sharply right</string>
|
||||||
|
<string name="dir_sharp_right_onto">Turn sharply right onto %1$s</string>
|
||||||
|
<string name="dir_u_turn">Make a U-turn</string>
|
||||||
|
<string name="dir_keep_left">Keep left</string>
|
||||||
|
<string name="dir_keep_left_toward">Keep left toward %1$s</string>
|
||||||
|
<string name="dir_keep_right">Keep right</string>
|
||||||
|
<string name="dir_keep_right_toward">Keep right toward %1$s</string>
|
||||||
|
<string name="dir_fork_left">Keep left at the fork</string>
|
||||||
|
<string name="dir_fork_left_toward">Keep left at the fork toward %1$s</string>
|
||||||
|
<string name="dir_fork_right">Keep right at the fork</string>
|
||||||
|
<string name="dir_fork_right_toward">Keep right at the fork toward %1$s</string>
|
||||||
|
<string name="dir_ramp_left">Take the ramp on the left</string>
|
||||||
|
<string name="dir_ramp_left_onto">Take the ramp on the left onto %1$s</string>
|
||||||
|
<string name="dir_ramp_right">Take the ramp on the right</string>
|
||||||
|
<string name="dir_ramp_right_onto">Take the ramp on the right onto %1$s</string>
|
||||||
|
<string name="dir_exit_left">Take the exit on the left</string>
|
||||||
|
<string name="dir_exit_left_toward">Take the exit on the left toward %1$s</string>
|
||||||
|
<string name="dir_exit_right">Take the exit on the right</string>
|
||||||
|
<string name="dir_exit_right_toward">Take the exit on the right toward %1$s</string>
|
||||||
|
<string name="dir_merge_left">Merge left</string>
|
||||||
|
<string name="dir_merge_left_onto">Merge left onto %1$s</string>
|
||||||
|
<string name="dir_merge_right">Merge right</string>
|
||||||
|
<string name="dir_merge_right_onto">Merge right onto %1$s</string>
|
||||||
|
<string name="dir_merge">Merge</string>
|
||||||
|
<string name="dir_merge_onto">Merge onto %1$s</string>
|
||||||
|
<string name="dir_roundabout_through">Go through the roundabout</string>
|
||||||
|
<string name="dir_roundabout_through_onto">Go through the roundabout onto %1$s</string>
|
||||||
|
<string name="dir_roundabout_enter">Enter the roundabout</string>
|
||||||
|
<string name="dir_roundabout_exit">Exit the roundabout</string>
|
||||||
|
<string name="dir_roundabout_exit_onto">Exit the roundabout onto %1$s</string>
|
||||||
|
<string name="dir_straight">Continue straight</string>
|
||||||
|
<string name="dir_straight_onto">Continue straight onto %1$s</string>
|
||||||
|
<string name="dir_continue">Continue</string>
|
||||||
|
<string name="dir_continue_onto">Continue onto %1$s</string>
|
||||||
|
<string name="dir_ferry">Take the ferry</string>
|
||||||
|
<string name="dir_train">Take the train</string>
|
||||||
|
<string name="dir_destination">Your destination is ahead</string>
|
||||||
|
<string name="dir_destination_left">Your destination is on the left</string>
|
||||||
|
<string name="dir_destination_right">Your destination is on the right</string>
|
||||||
|
<string name="dir_roundabout_take">At the roundabout, take the %1$s exit</string>
|
||||||
|
<string name="dir_roundabout_take_onto">At the roundabout, take the %1$s exit onto %2$s</string>
|
||||||
|
<string name="dir_ordinal_1">first</string>
|
||||||
|
<string name="dir_ordinal_2">second</string>
|
||||||
|
<string name="dir_ordinal_3">third</string>
|
||||||
|
<string name="dir_ordinal_4">fourth</string>
|
||||||
|
<string name="dir_ordinal_5">fifth</string>
|
||||||
|
<string name="dir_ordinal_6">sixth</string>
|
||||||
|
<string name="dir_ordinal_7">seventh</string>
|
||||||
|
<string name="dir_ordinal_8">eighth</string>
|
||||||
|
<string name="dir_in_distance">In %1$s, %2$s</string>
|
||||||
|
<plurals name="dir_km">
|
||||||
|
<item quantity="one">%1$s kilometer</item>
|
||||||
|
<item quantity="other">%1$s kilometers</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="dir_km_decimal">%1$s kilometers</string>
|
||||||
|
<string name="dir_quarter_mile">a quarter mile</string>
|
||||||
|
<string name="dir_half_mile">half a mile</string>
|
||||||
|
<string name="dir_three_quarter_mile">three quarters of a mile</string>
|
||||||
|
<plurals name="dir_miles">
|
||||||
|
<item quantity="one">%1$s mile</item>
|
||||||
|
<item quantity="other">%1$s miles</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="dir_miles_decimal">%1$s miles</string>
|
||||||
|
<plurals name="dir_feet">
|
||||||
|
<item quantity="one">%1$s foot</item>
|
||||||
|
<item quantity="other">%1$s feet</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="dir_yards">
|
||||||
|
<item quantity="one">%1$s yard</item>
|
||||||
|
<item quantity="other">%1$s yards</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="dir_meters">
|
||||||
|
<item quantity="one">%1$d meter</item>
|
||||||
|
<item quantity="other">%1$d meters</item>
|
||||||
|
</plurals>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in new issue
Block a user