feat(login): replace Facebook with email-password auth screen
- Remove Facebook provider from Login flow - Add EmailLoginScreen with gradient background, email/password fields - Add EmailLoginScreenContract and EmailLoginViewModel with stub logic - Add navigation: Sign in with email -> EmailLoginScreen - Use back arrow icon instead of back text - Move header above email field, add Register button - Update run command to launch app after install - Add ic_arrow_back drawable, update strings Made-with: Cursor
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
# Run app on device
|
||||
|
||||
Build and install the app on the connected USB Android device.
|
||||
Build, install, and launch the app on the connected USB Android device.
|
||||
|
||||
## What to do
|
||||
|
||||
1. Run `./gradlew installDebug` (use `gradlew.bat` on Windows).
|
||||
2. Ensure the device is connected via USB with USB debugging enabled.
|
||||
3. After a successful install, report the device name and that the app is ready to launch.
|
||||
3. After a successful install, launch the app with `adb shell am start -n org.db3.airmq/.MainActivity`.
|
||||
4. Report the device name and that the app was launched.
|
||||
|
||||
@@ -413,11 +413,11 @@ private fun AirMQButtonsPreviewSocial() {
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
AirMQSocialButton(
|
||||
text = "Sign in with Facebook",
|
||||
leadingIconRes = R.drawable.ic_facebook,
|
||||
iconTint = Color.White,
|
||||
containerColor = Color(0xFF3B5998),
|
||||
contentColor = Color.White,
|
||||
text = "Sign in with email",
|
||||
leadingIconRes = R.drawable.ic_account,
|
||||
iconTint = Color.Unspecified,
|
||||
containerColor = Color.White,
|
||||
contentColor = Color(0xFF202124),
|
||||
onClick = {},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
package org.db3.airmq.features.login
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import org.db3.airmq.R
|
||||
import androidx.compose.material3.TextButton
|
||||
import org.db3.airmq.features.common.AirMQOutlinedLightButton
|
||||
import org.db3.airmq.features.login.EmailLoginScreenContract.Action
|
||||
import org.db3.airmq.features.login.EmailLoginScreenContract.Event
|
||||
import org.db3.airmq.features.login.EmailLoginScreenContract.State
|
||||
import org.db3.airmq.ui.theme.AirMQTheme
|
||||
|
||||
private val LegacyLoginGradientStart = Color(0xFF449CF5)
|
||||
private val LegacyLoginGradientEnd = Color(0xFF5CE4BB)
|
||||
|
||||
@Composable
|
||||
fun EmailLoginScreen(
|
||||
onLogInToManage: () -> Unit,
|
||||
onBack: () -> Unit,
|
||||
viewModel: EmailLoginViewModel = hiltViewModel()
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val context = LocalContext.current
|
||||
|
||||
LaunchedEffect(viewModel) {
|
||||
viewModel.actions.collectLatest { action ->
|
||||
when (action) {
|
||||
Action.OpenManage -> onLogInToManage()
|
||||
is Action.ShowMessage -> {
|
||||
Toast.makeText(context, action.message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EmailLoginScreenContent(
|
||||
uiState = uiState,
|
||||
onEvent = viewModel::onEvent,
|
||||
onBack = onBack
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmailLoginScreenContent(
|
||||
uiState: State,
|
||||
onEvent: (Event) -> Unit,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
brush = Brush.linearGradient(
|
||||
colorStops = arrayOf(
|
||||
0.0f to LegacyLoginGradientStart,
|
||||
0.35f to LegacyLoginGradientStart,
|
||||
1.0f to LegacyLoginGradientEnd
|
||||
),
|
||||
start = Offset(0f, 0f),
|
||||
end = Offset(1200f, 1200f)
|
||||
)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.statusBarsPadding()
|
||||
.padding(horizontal = 40.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_arrow_back),
|
||||
contentDescription = stringResource(id = R.string.content_back),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxWidth(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.text_sign_in_email),
|
||||
color = Color.White,
|
||||
fontSize = 36.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = uiState.email,
|
||||
onValueChange = { onEvent(Event.EmailChanged(it)) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = {
|
||||
Text(
|
||||
stringResource(id = R.string.hint_email),
|
||||
color = Color.White.copy(alpha = 0.7f)
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Email,
|
||||
imeAction = ImeAction.Next
|
||||
),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedTextColor = Color.White,
|
||||
unfocusedTextColor = Color.White,
|
||||
cursorColor = Color.White,
|
||||
focusedBorderColor = Color.White.copy(alpha = 0.8f),
|
||||
unfocusedBorderColor = Color.White.copy(alpha = 0.55f),
|
||||
focusedContainerColor = Color.White.copy(alpha = 0.1f),
|
||||
unfocusedContainerColor = Color.White.copy(alpha = 0.05f)
|
||||
)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = uiState.password,
|
||||
onValueChange = { onEvent(Event.PasswordChanged(it)) },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = {
|
||||
Text(
|
||||
stringResource(id = R.string.hint_password),
|
||||
color = Color.White.copy(alpha = 0.7f)
|
||||
)
|
||||
},
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Done
|
||||
),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedTextColor = Color.White,
|
||||
unfocusedTextColor = Color.White,
|
||||
cursorColor = Color.White,
|
||||
focusedBorderColor = Color.White.copy(alpha = 0.8f),
|
||||
unfocusedBorderColor = Color.White.copy(alpha = 0.55f),
|
||||
focusedContainerColor = Color.White.copy(alpha = 0.1f),
|
||||
unfocusedContainerColor = Color.White.copy(alpha = 0.05f)
|
||||
)
|
||||
)
|
||||
|
||||
if (uiState.isLoading) {
|
||||
CircularProgressIndicator(color = Color.White)
|
||||
} else {
|
||||
AirMQOutlinedLightButton(
|
||||
text = stringResource(id = R.string.button_sign_in),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = { onEvent(Event.SignInClicked) }
|
||||
)
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = { onEvent(Event.RegisterClicked) },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.button_register),
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun EmailLoginScreenPreview() {
|
||||
AirMQTheme {
|
||||
EmailLoginScreenContent(
|
||||
uiState = State(),
|
||||
onEvent = {},
|
||||
onBack = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.db3.airmq.features.login
|
||||
|
||||
object EmailLoginScreenContract {
|
||||
|
||||
data class State(
|
||||
val email: String = "",
|
||||
val password: String = "",
|
||||
val isLoading: Boolean = false
|
||||
)
|
||||
|
||||
sealed interface Action {
|
||||
data object OpenManage : Action
|
||||
data class ShowMessage(val message: String) : Action
|
||||
}
|
||||
|
||||
sealed interface Event {
|
||||
data class EmailChanged(val value: String) : Event
|
||||
data class PasswordChanged(val value: String) : Event
|
||||
data object SignInClicked : Event
|
||||
data object RegisterClicked : Event
|
||||
data object BackClicked : Event
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.db3.airmq.features.login
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import org.db3.airmq.R
|
||||
import org.db3.airmq.features.login.EmailLoginScreenContract.Action
|
||||
import org.db3.airmq.features.login.EmailLoginScreenContract.Event
|
||||
import org.db3.airmq.features.login.EmailLoginScreenContract.State
|
||||
|
||||
@HiltViewModel
|
||||
class EmailLoginViewModel @Inject constructor(
|
||||
@ApplicationContext private val appContext: Context
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(State())
|
||||
val uiState: StateFlow<State> = _uiState.asStateFlow()
|
||||
|
||||
private val _actions = MutableSharedFlow<Action>(extraBufferCapacity = 1)
|
||||
val actions: SharedFlow<Action> = _actions.asSharedFlow()
|
||||
|
||||
fun onEvent(event: Event) {
|
||||
when (event) {
|
||||
is Event.EmailChanged -> {
|
||||
_uiState.value = _uiState.value.copy(email = event.value)
|
||||
}
|
||||
is Event.PasswordChanged -> {
|
||||
_uiState.value = _uiState.value.copy(password = event.value)
|
||||
}
|
||||
Event.SignInClicked -> {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
||||
_actions.tryEmit(
|
||||
Action.ShowMessage(appContext.getString(R.string.coming_soon))
|
||||
)
|
||||
_uiState.value = _uiState.value.copy(isLoading = false)
|
||||
}
|
||||
Event.RegisterClicked -> {
|
||||
_actions.tryEmit(
|
||||
Action.ShowMessage(appContext.getString(R.string.coming_soon))
|
||||
)
|
||||
}
|
||||
Event.BackClicked -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,11 +56,11 @@ import org.db3.airmq.ui.theme.AirMQTheme
|
||||
|
||||
private val LegacyLoginGradientStart = Color(0xFF449CF5)
|
||||
private val LegacyLoginGradientEnd = Color(0xFF5CE4BB)
|
||||
private val LegacyFacebookBlue = Color(0xFF3B5998)
|
||||
|
||||
@Composable
|
||||
fun LoginScreen(
|
||||
onLogInToManage: () -> Unit,
|
||||
onOpenEmailLogin: () -> Unit,
|
||||
viewModel: LoginViewModel = hiltViewModel()
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
@@ -78,6 +78,7 @@ fun LoginScreen(
|
||||
is GoogleSignInResult.Error -> viewModel.onEvent(Event.GoogleSignInFailed(result.message))
|
||||
}
|
||||
}
|
||||
Action.NavigateToEmailLogin -> onOpenEmailLogin()
|
||||
Action.ShowContinueAnonymousDialog -> showContinueAnonymousDialog = true
|
||||
Action.OpenPrivacyPolicy -> {
|
||||
Toast.makeText(context, context.getString(R.string.coming_soon), Toast.LENGTH_SHORT).show()
|
||||
@@ -216,13 +217,13 @@ private fun LoginScreenContent(
|
||||
)
|
||||
|
||||
AirMQSocialButton(
|
||||
text = stringResource(id = R.string.button_sign_in_facebook),
|
||||
leadingIconRes = R.drawable.ic_facebook,
|
||||
iconTint = Color.White,
|
||||
containerColor = LegacyFacebookBlue,
|
||||
contentColor = Color.White,
|
||||
text = stringResource(id = R.string.button_sign_in_email),
|
||||
leadingIconRes = R.drawable.ic_account,
|
||||
iconTint = Color.Unspecified,
|
||||
containerColor = Color.White,
|
||||
contentColor = Color(0xFF202124),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = { onEvent(Event.FacebookClicked) }
|
||||
onClick = { onEvent(Event.EmailClicked) }
|
||||
)
|
||||
|
||||
AirMQOutlinedLightButton(
|
||||
|
||||
@@ -9,6 +9,7 @@ object LoginScreenContract {
|
||||
sealed interface Action {
|
||||
data object OpenManage : Action
|
||||
data object LaunchGoogleSignIn : Action
|
||||
data object NavigateToEmailLogin : Action
|
||||
data object ShowContinueAnonymousDialog : Action
|
||||
data object OpenPrivacyPolicy : Action
|
||||
data object OpenTermsAndConditions : Action
|
||||
@@ -20,7 +21,7 @@ object LoginScreenContract {
|
||||
data class GoogleTokenReceived(val idToken: String) : Event
|
||||
data object GoogleSignInCancelled : Event
|
||||
data class GoogleSignInFailed(val message: String? = null) : Event
|
||||
data object FacebookClicked : Event
|
||||
data object EmailClicked : Event
|
||||
data object ContinueAnonymousClicked : Event
|
||||
data object ContinueAnonymousConfirmed : Event
|
||||
data object ContinueAnonymousDismissed : Event
|
||||
|
||||
@@ -52,8 +52,8 @@ class LoginViewModel @Inject constructor(
|
||||
Event.GoogleSignInCancelled -> {
|
||||
_uiState.value = _uiState.value.copy(isLoading = false)
|
||||
}
|
||||
Event.FacebookClicked -> {
|
||||
_actions.tryEmit(Action.ShowMessage(appContext.getString(R.string.coming_soon)))
|
||||
Event.EmailClicked -> {
|
||||
_actions.tryEmit(Action.NavigateToEmailLogin)
|
||||
}
|
||||
Event.ContinueAnonymousClicked -> {
|
||||
_actions.tryEmit(Action.ShowContinueAnonymousDialog)
|
||||
|
||||
@@ -38,6 +38,7 @@ import org.db3.airmq.features.device.DeviceScreen
|
||||
import org.db3.airmq.features.entry.SplashScreen
|
||||
import org.db3.airmq.features.entry.WizardScreen
|
||||
import org.db3.airmq.features.location.LocationScreen
|
||||
import org.db3.airmq.features.login.EmailLoginScreen
|
||||
import org.db3.airmq.features.login.LoginScreen
|
||||
import org.db3.airmq.features.manage.ManageScreen
|
||||
import org.db3.airmq.features.map.MapScreen
|
||||
@@ -204,7 +205,18 @@ fun AirMQNavGraph(modifier: Modifier = Modifier) {
|
||||
}
|
||||
composable(AirMqRoutes.LOGIN) {
|
||||
LoginScreen(
|
||||
onLogInToManage = { navController.navigate(AirMqRoutes.MANAGE) }
|
||||
onLogInToManage = { navController.navigate(AirMqRoutes.MANAGE) },
|
||||
onOpenEmailLogin = { navController.navigate(AirMqRoutes.EMAIL_LOGIN) }
|
||||
)
|
||||
}
|
||||
composable(AirMqRoutes.EMAIL_LOGIN) {
|
||||
EmailLoginScreen(
|
||||
onLogInToManage = {
|
||||
navController.navigate(AirMqRoutes.MANAGE) {
|
||||
popUpTo(AirMqRoutes.LOGIN) { inclusive = true }
|
||||
}
|
||||
},
|
||||
onBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
composable(AirMqRoutes.NEWS) {
|
||||
|
||||
@@ -12,6 +12,7 @@ object AirMqRoutes {
|
||||
const val LOCATION = "detail/location"
|
||||
const val SETUP = "detail/setup"
|
||||
const val LOGIN = "detail/login"
|
||||
const val EMAIL_LOGIN = "detail/email-login"
|
||||
const val NEWS = "detail/news"
|
||||
const val NEWS_DETAIL = "detail/news/{newsId}"
|
||||
const val DEVICE = "detail/device/{deviceId}"
|
||||
|
||||
9
app/src/main/res/drawable/ic_arrow_back.xml
Normal file
9
app/src/main/res/drawable/ic_arrow_back.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
|
||||
</vector>
|
||||
@@ -23,7 +23,7 @@
|
||||
<string name="button_cancel">Адмяніць</string>
|
||||
<string name="text_privacy_policy"><b><u>Палітыка канфідэнцыйнасці</u></b></string>
|
||||
<string name="button_sign_in_google">Увайсці з дапамогай Google</string>
|
||||
<string name="button_sign_in_facebook">Увайсці з дапамогай Facebook</string>
|
||||
<string name="button_sign_in_email">Увайсці па email</string>
|
||||
<string name="button_look_for_device">Пошук прылады</string>
|
||||
<string name="button_finish">Завяршыць</string>
|
||||
<string name="button_continue_anonym">Працягнуць ананімна</string>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<string name="button_cancel">Отменить</string>
|
||||
<string name="text_privacy_policy"><b><u>Политикой конфиденциальности</u></b></string>
|
||||
<string name="button_sign_in_google">Войти с помощью Google</string>
|
||||
<string name="button_sign_in_facebook">Войти с помощью Facebook</string>
|
||||
<string name="button_sign_in_email">Войти по email</string>
|
||||
<string name="button_look_for_device">Поиск устройства</string>
|
||||
<string name="button_finish">Завершить</string>
|
||||
<string name="button_continue_anonym">Продолжить анонимно</string>
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
<string name="button_cancel">Cancel</string>
|
||||
<string name="button_register">Register</string>
|
||||
<string name="button_sign_in_google">Sign in with Google</string>
|
||||
<string name="button_sign_in_facebook">Sign in with Facebook</string>
|
||||
<string name="button_sign_in_email">Sign in with email</string>
|
||||
<string name="button_copy_log">Copy log</string>
|
||||
<string name="button_look_for_device">Look for a device</string>
|
||||
<string name="button_finish">Finish</string>
|
||||
@@ -92,7 +92,10 @@
|
||||
<string name="button_setup">Add new device</string>
|
||||
<string name="button_connecting">Connecting…</string>
|
||||
|
||||
<string name="text_sign_in_email">Sign in with email</string>
|
||||
|
||||
<!-- Hints -->
|
||||
<string name="hint_email">Email</string>
|
||||
<string name="hint_password">Password</string>
|
||||
<string name="hint_device_name">Device name</string>
|
||||
<string name="hint_setup_name">Name</string>
|
||||
@@ -142,8 +145,6 @@
|
||||
<string name="toast_copied">Copied</string>
|
||||
<string name="snackbar_not_registered">Device requires registration</string>
|
||||
|
||||
<string name="facebook_app_id" translatable="false">356744979027664</string>
|
||||
<string name="fb_login_protocol_scheme" translatable="false">fb356744979027664</string>
|
||||
<string name="text_air_quality">Air quality</string>
|
||||
<string name="pref_fullscreen">Borderless layout</string>
|
||||
<string name="text_error">Error</string>
|
||||
|
||||
Reference in New Issue
Block a user