mirror of
https://github.com/bitwarden/android.git
synced 2026-08-29 10:17:56 -05:00
Display no items content when user has no items saved (#13)
This commit is contained in:
+75
-74
@@ -1,10 +1,7 @@
|
||||
package com.x8bit.bitwarden.authenticator.data.authenticator.datasource.disk
|
||||
|
||||
import com.bitwarden.core.CipherRepromptType
|
||||
import com.bitwarden.core.CipherType
|
||||
import com.bitwarden.core.CipherView
|
||||
import com.bitwarden.core.DateTime
|
||||
import com.bitwarden.core.LoginView
|
||||
import com.x8bit.bitwarden.authenticator.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import javax.inject.Inject
|
||||
@@ -12,9 +9,11 @@ import javax.inject.Inject
|
||||
class AuthenticatorDiskSourceImpl @Inject constructor() : AuthenticatorDiskSource {
|
||||
|
||||
private val ciphers = STATIC_CIPHER_CACHE.toMutableList()
|
||||
private val mutableCiphersFlow = bufferedMutableSharedFlow<List<CipherView>>()
|
||||
|
||||
override suspend fun saveCipher(cipher: CipherView) {
|
||||
ciphers.add(cipher)
|
||||
mutableCiphersFlow.tryEmit(ciphers)
|
||||
}
|
||||
|
||||
override fun getCiphers(): Flow<List<CipherView>> {
|
||||
@@ -23,76 +22,78 @@ class AuthenticatorDiskSourceImpl @Inject constructor() : AuthenticatorDiskSourc
|
||||
|
||||
override suspend fun deleteCipher(cipherId: String) {
|
||||
ciphers.removeIf { it.id == cipherId }
|
||||
mutableCiphersFlow.tryEmit(ciphers)
|
||||
}
|
||||
}
|
||||
|
||||
private val STATIC_CIPHER_CACHE = listOf(
|
||||
CipherView(
|
||||
id = "1",
|
||||
organizationId = null,
|
||||
folderId = null,
|
||||
collectionIds = emptyList(),
|
||||
key = null,
|
||||
name = "TOTP test 1",
|
||||
notes = null,
|
||||
type = CipherType.LOGIN,
|
||||
login = LoginView(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"JBSWY3DPEHPK3PXP",
|
||||
null,
|
||||
null
|
||||
),
|
||||
identity = null,
|
||||
card = null,
|
||||
secureNote = null,
|
||||
favorite = true,
|
||||
reprompt = CipherRepromptType.NONE,
|
||||
organizationUseTotp = false,
|
||||
edit = false,
|
||||
viewPassword = true,
|
||||
localData = null,
|
||||
attachments = null,
|
||||
fields = null,
|
||||
passwordHistory = null,
|
||||
creationDate = DateTime.now(),
|
||||
deletedDate = null,
|
||||
revisionDate = DateTime.now()
|
||||
),
|
||||
CipherView(
|
||||
id = "2",
|
||||
organizationId = null,
|
||||
folderId = null,
|
||||
collectionIds = emptyList(),
|
||||
key = null,
|
||||
name = "TOTP test 2",
|
||||
notes = null,
|
||||
type = CipherType.LOGIN,
|
||||
login = LoginView(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"JBSWY3DPEHPK3PXP",
|
||||
null,
|
||||
null
|
||||
),
|
||||
identity = null,
|
||||
card = null,
|
||||
secureNote = null,
|
||||
favorite = true,
|
||||
reprompt = CipherRepromptType.NONE,
|
||||
organizationUseTotp = false,
|
||||
edit = false,
|
||||
viewPassword = true,
|
||||
localData = null,
|
||||
attachments = null,
|
||||
fields = null,
|
||||
passwordHistory = null,
|
||||
creationDate = DateTime.now(),
|
||||
deletedDate = null,
|
||||
revisionDate = DateTime.now()
|
||||
)
|
||||
)
|
||||
private val STATIC_CIPHER_CACHE = emptyList<CipherView>()
|
||||
// listOf(
|
||||
// CipherView(
|
||||
// id = "1",
|
||||
// organizationId = null,
|
||||
// folderId = null,
|
||||
// collectionIds = emptyList(),
|
||||
// key = null,
|
||||
// name = "TOTP test 1",
|
||||
// notes = null,
|
||||
// type = CipherType.LOGIN,
|
||||
// login = LoginView(
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// "JBSWY3DPEHPK3PXP",
|
||||
// null,
|
||||
// null
|
||||
// ),
|
||||
// identity = null,
|
||||
// card = null,
|
||||
// secureNote = null,
|
||||
// favorite = true,
|
||||
// reprompt = CipherRepromptType.NONE,
|
||||
// organizationUseTotp = false,
|
||||
// edit = false,
|
||||
// viewPassword = true,
|
||||
// localData = null,
|
||||
// attachments = null,
|
||||
// fields = null,
|
||||
// passwordHistory = null,
|
||||
// creationDate = DateTime.now(),
|
||||
// deletedDate = null,
|
||||
// revisionDate = DateTime.now()
|
||||
// ),
|
||||
// CipherView(
|
||||
// id = "2",
|
||||
// organizationId = null,
|
||||
// folderId = null,
|
||||
// collectionIds = emptyList(),
|
||||
// key = null,
|
||||
// name = "TOTP test 2",
|
||||
// notes = null,
|
||||
// type = CipherType.LOGIN,
|
||||
// login = LoginView(
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// null,
|
||||
// "JBSWY3DPEHPK3PXP",
|
||||
// null,
|
||||
// null
|
||||
// ),
|
||||
// identity = null,
|
||||
// card = null,
|
||||
// secureNote = null,
|
||||
// favorite = true,
|
||||
// reprompt = CipherRepromptType.NONE,
|
||||
// organizationUseTotp = false,
|
||||
// edit = false,
|
||||
// viewPassword = true,
|
||||
// localData = null,
|
||||
// attachments = null,
|
||||
// fields = null,
|
||||
// passwordHistory = null,
|
||||
// creationDate = DateTime.now(),
|
||||
// deletedDate = null,
|
||||
// revisionDate = DateTime.now()
|
||||
// )
|
||||
//)
|
||||
|
||||
+4
-5
@@ -36,7 +36,7 @@ import javax.inject.Inject
|
||||
* A "stop timeout delay" in milliseconds used to let a shared coroutine continue to run for the
|
||||
* specified period of time after it no longer has subscribers.
|
||||
*/
|
||||
private const val STOP_TIMEOUT_DELAY_MS: Long = 1_000L
|
||||
private const val STOP_TIMEOUT_DELAY_MS: Long = 5_000L
|
||||
|
||||
class AuthenticatorRepositoryImpl @Inject constructor(
|
||||
private val authenticatorDiskSource: AuthenticatorDiskSource,
|
||||
@@ -57,7 +57,6 @@ class AuthenticatorRepositoryImpl @Inject constructor(
|
||||
|
||||
override val authenticatorDataFlow: StateFlow<DataState<AuthenticatorData>> =
|
||||
ciphersStateFlow.map { cipherDataState ->
|
||||
|
||||
when (cipherDataState) {
|
||||
is DataState.Error -> {
|
||||
DataState.Error(
|
||||
@@ -114,7 +113,7 @@ class AuthenticatorRepositoryImpl @Inject constructor(
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.Lazily,
|
||||
started = SharingStarted.WhileSubscribed(STOP_TIMEOUT_DELAY_MS),
|
||||
initialValue = DataState.Loading,
|
||||
)
|
||||
|
||||
@@ -139,7 +138,7 @@ class AuthenticatorRepositoryImpl @Inject constructor(
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
started = SharingStarted.WhileSubscribed(STOP_TIMEOUT_DELAY_MS),
|
||||
initialValue = DataState.Loading,
|
||||
)
|
||||
}
|
||||
@@ -177,7 +176,7 @@ class AuthenticatorRepositoryImpl @Inject constructor(
|
||||
}
|
||||
.stateIn(
|
||||
scope = unconfinedScope,
|
||||
started = SharingStarted.WhileSubscribed(),
|
||||
started = SharingStarted.WhileSubscribed(STOP_TIMEOUT_DELAY_MS),
|
||||
initialValue = DataState.Loading,
|
||||
)
|
||||
}
|
||||
|
||||
+4
@@ -29,6 +29,10 @@ fun NavGraphBuilder.itemListingGraph(
|
||||
onNavigateToItemScreen = { navController.navigateToItem(itemId = it) },
|
||||
onNavigateToEditItemScreen = { /*navController.navigateToEditItem(itemId = it)*/ },
|
||||
onNavigateToManualKeyEntry = { navController.navigateToManualCodeEntryScreen() },
|
||||
onNavigateToSyncWithBitwardenScreen = {
|
||||
/*navController.navigateToSyncWithBitwardenScreen()*/
|
||||
},
|
||||
onNavigateToImportScreen = { /*navController.navigateToImportScreen()*/ }
|
||||
)
|
||||
itemDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
|
||||
+5
-1
@@ -14,6 +14,8 @@ fun NavGraphBuilder.itemListingDestination(
|
||||
onNavigateToManualKeyEntry: () -> Unit = { },
|
||||
onNavigateToItemScreen: (id: String) -> Unit = { },
|
||||
onNavigateToEditItemScreen: (id: String) -> Unit = { },
|
||||
onNavigateToSyncWithBitwardenScreen: () -> Unit = { },
|
||||
onNavigateToImportScreen: () -> Unit = { },
|
||||
) {
|
||||
composableWithPushTransitions(
|
||||
route = ITEM_LIST_ROUTE,
|
||||
@@ -23,7 +25,9 @@ fun NavGraphBuilder.itemListingDestination(
|
||||
onNavigateToQrCodeScanner = onNavigateToQrCodeScanner,
|
||||
onNavigateToManualKeyEntry = onNavigateToManualKeyEntry,
|
||||
onNavigateToItemScreen = onNavigateToItemScreen,
|
||||
onNavigateToEditItemScreen = onNavigateToEditItemScreen
|
||||
onNavigateToEditItemScreen = onNavigateToEditItemScreen,
|
||||
onNavigateToSyncWithBitwardenScreen = onNavigateToSyncWithBitwardenScreen,
|
||||
onNavigateToImportScreen = onNavigateToImportScreen
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+103
-17
@@ -1,9 +1,13 @@
|
||||
package com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -15,13 +19,17 @@ import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@@ -30,6 +38,8 @@ import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.mo
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.button.BitwardenFilledTonalButton
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.button.BitwardenTextButton
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.dialog.BasicDialogState
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.dialog.BitwardenLoadingDialog
|
||||
@@ -38,6 +48,7 @@ import com.x8bit.bitwarden.authenticator.ui.platform.components.fab.ExpandableFa
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.fab.ExpandableFloatingActionButton
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.model.IconResource
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.theme.Typography
|
||||
|
||||
/**
|
||||
* Displays the item listing screen.
|
||||
@@ -51,6 +62,8 @@ fun ItemListingScreen(
|
||||
onNavigateToManualKeyEntry: () -> Unit,
|
||||
onNavigateToItemScreen: (id: String) -> Unit,
|
||||
onNavigateToEditItemScreen: (id: String) -> Unit,
|
||||
onNavigateToSyncWithBitwardenScreen: () -> Unit,
|
||||
onNavigateToImportScreen: () -> Unit,
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||
@@ -65,7 +78,12 @@ fun ItemListingScreen(
|
||||
ItemListingEvent.NavigateToManualAddItem -> onNavigateToManualKeyEntry()
|
||||
is ItemListingEvent.NavigateToItem -> onNavigateToItemScreen(event.id)
|
||||
is ItemListingEvent.ShowToast -> {
|
||||
Toast.makeText(context, event.message(context.resources), Toast.LENGTH_LONG)
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
event.message(context.resources),
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
.show()
|
||||
}
|
||||
|
||||
@@ -96,25 +114,21 @@ fun ItemListingScreen(
|
||||
icon = IconResource(
|
||||
iconPainter = painterResource(id = R.drawable.ic_camera),
|
||||
contentDescription = stringResource(id = R.string.scan_a_qr_code),
|
||||
testTag = "ScanQRCodeButton"
|
||||
testTag = "ScanQRCodeButton",
|
||||
),
|
||||
onScanQrCodeClick = {
|
||||
viewModel.trySendAction(
|
||||
ItemListingAction.ScanQrCodeClick
|
||||
)
|
||||
viewModel.trySendAction(ItemListingAction.ScanQrCodeClick)
|
||||
}
|
||||
),
|
||||
ItemListingExpandableFabAction.EnterSetupKey(
|
||||
label = R.string.enter_a_setup_key.asText(),
|
||||
icon = IconResource(
|
||||
iconPainter = painterResource(id = R.drawable.ic_copy),
|
||||
iconPainter = painterResource(id = R.drawable.ic_keyboard_24px),
|
||||
contentDescription = stringResource(id = R.string.enter_a_setup_key),
|
||||
testTag = "EnterSetupKeyButton"
|
||||
testTag = "EnterSetupKeyButton",
|
||||
),
|
||||
onEnterSetupKeyClick = {
|
||||
viewModel.trySendAction(
|
||||
ItemListingAction.EnterSetupKeyClick
|
||||
)
|
||||
viewModel.trySendAction(ItemListingAction.EnterSetupKeyClick)
|
||||
}
|
||||
)
|
||||
),
|
||||
@@ -161,15 +175,20 @@ fun ItemListingScreen(
|
||||
}
|
||||
|
||||
is ItemListingState.ViewState.Error -> {
|
||||
Text(text = "Error! ${currentState.message}", modifier = Modifier.fillMaxSize())
|
||||
Text(
|
||||
text = "Error! ${currentState.message}",
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
|
||||
ItemListingState.ViewState.Loading -> {
|
||||
Text(text = "Loading")
|
||||
}
|
||||
|
||||
ItemListingState.ViewState.NoItems -> {
|
||||
Text(text = "Welcome! Add a 2FA TOTP", modifier = Modifier.fillMaxSize())
|
||||
ItemListingState.ViewState.NoItems,
|
||||
ItemListingState.ViewState.Loading,
|
||||
-> {
|
||||
EmptyItemListingContent(
|
||||
onAddCodeClick = onNavigateToQrCodeScanner,
|
||||
onSyncWithBitwardenClick = onNavigateToSyncWithBitwardenScreen,
|
||||
onImportItemsClick = onNavigateToImportScreen,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,3 +218,70 @@ fun ItemListingScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the item listing screen with no existing items.
|
||||
*/
|
||||
@Composable
|
||||
fun EmptyItemListingContent(
|
||||
modifier: Modifier = Modifier,
|
||||
onAddCodeClick: () -> Unit = {},
|
||||
onSyncWithBitwardenClick: () -> Unit = {},
|
||||
onImportItemsClick: () -> Unit = {},
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Image(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
painter = painterResource(id = R.drawable.ic_empty_vault),
|
||||
contentDescription = stringResource(
|
||||
id = R.string.empty_item_list,
|
||||
),
|
||||
contentScale = ContentScale.Fit,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.you_dont_have_items_to_display),
|
||||
style = Typography.titleMedium,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
textAlign = TextAlign.Center,
|
||||
text = stringResource(id = R.string.empty_item_list_instruction),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BitwardenFilledTonalButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
label = stringResource(R.string.add_code),
|
||||
onClick = onAddCodeClick,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = R.string.sync_items_with_bitwarden),
|
||||
onClick = onSyncWithBitwardenClick,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BitwardenTextButton(
|
||||
label = stringResource(id = R.string.import_items),
|
||||
onClick = onImportItemsClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(showBackground = true)
|
||||
fun EmptyListingContentPreview() {
|
||||
EmptyItemListingContent(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
)
|
||||
}
|
||||
|
||||
+140
-136
@@ -7,14 +7,15 @@ import com.x8bit.bitwarden.authenticator.data.authenticator.manager.model.Verifi
|
||||
import com.x8bit.bitwarden.authenticator.data.authenticator.repository.AuthenticatorRepository
|
||||
import com.x8bit.bitwarden.authenticator.data.platform.repository.SettingsRepository
|
||||
import com.x8bit.bitwarden.authenticator.data.platform.repository.model.DataState
|
||||
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.model.ItemListingData
|
||||
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.util.toViewState
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.Text
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.base.util.concat
|
||||
import com.x8bit.bitwarden.authenticator.ui.platform.components.model.IconData
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.parcelize.Parcelize
|
||||
@@ -27,77 +28,75 @@ import javax.inject.Inject
|
||||
class ItemListingViewModel @Inject constructor(
|
||||
authenticatorRepository: AuthenticatorRepository,
|
||||
settingsRepository: SettingsRepository,
|
||||
) :
|
||||
BaseViewModel<ItemListingState, ItemListingEvent, ItemListingAction>(
|
||||
initialState = ItemListingState(
|
||||
viewState = ItemListingState.ViewState.Loading,
|
||||
dialog = null
|
||||
)
|
||||
) {
|
||||
) : BaseViewModel<ItemListingState, ItemListingEvent, ItemListingAction>(
|
||||
initialState = ItemListingState(
|
||||
settingsRepository.authenticatorAlertThresholdSeconds,
|
||||
viewState = ItemListingState.ViewState.Loading,
|
||||
dialog = null
|
||||
)
|
||||
) {
|
||||
|
||||
init {
|
||||
combine(
|
||||
authenticatorRepository.getAuthCodesFlow(),
|
||||
settingsRepository.authenticatorAlertThresholdSecondsFlow,
|
||||
) { authCodeState, alertThresholdSeconds ->
|
||||
when (authCodeState) {
|
||||
is DataState.Error -> {
|
||||
DataState.Error(
|
||||
error = authCodeState.error,
|
||||
data = ItemListingData(
|
||||
alertThresholdSeconds = alertThresholdSeconds,
|
||||
authenticatorData = authCodeState.data,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is DataState.Loaded -> {
|
||||
DataState.Loaded(
|
||||
ItemListingData(
|
||||
alertThresholdSeconds = alertThresholdSeconds,
|
||||
authCodeState.data
|
||||
)
|
||||
)
|
||||
}
|
||||
settingsRepository
|
||||
.authenticatorAlertThresholdSecondsFlow
|
||||
.map { ItemListingAction.Internal.AlertThresholdSecondsReceive(it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
DataState.Loading -> {
|
||||
DataState.Loading
|
||||
}
|
||||
|
||||
is DataState.NoNetwork -> {
|
||||
DataState.NoNetwork(
|
||||
ItemListingData(
|
||||
alertThresholdSeconds = alertThresholdSeconds,
|
||||
authenticatorData = authCodeState.data ?: emptyList()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
is DataState.Pending -> {
|
||||
DataState.Pending(
|
||||
ItemListingData(
|
||||
alertThresholdSeconds = alertThresholdSeconds,
|
||||
authCodeState.data
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onEach {
|
||||
sendAction(ItemListingAction.Internal.AuthenticatorDataReceive(it))
|
||||
}
|
||||
authenticatorRepository
|
||||
.getAuthCodesFlow()
|
||||
.map { ItemListingAction.Internal.AuthCodesUpdated(it) }
|
||||
.onEach(::sendAction)
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: ItemListingAction) {
|
||||
when (action) {
|
||||
ItemListingAction.ScanQrCodeClick -> sendEvent(ItemListingEvent.NavigateToQrCodeScanner)
|
||||
ItemListingAction.EnterSetupKeyClick -> sendEvent(ItemListingEvent.NavigateToManualAddItem)
|
||||
ItemListingAction.BackClick -> sendEvent(ItemListingEvent.NavigateBack)
|
||||
is ItemListingAction.ItemClick -> sendEvent(ItemListingEvent.NavigateToItem(action.id))
|
||||
ItemListingAction.DialogDismiss -> handleDialogDismiss()
|
||||
is ItemListingAction.Internal.AuthenticatorDataReceive -> handleAuthenticatorDataReceive(
|
||||
action
|
||||
ItemListingAction.ScanQrCodeClick -> {
|
||||
sendEvent(ItemListingEvent.NavigateToQrCodeScanner)
|
||||
}
|
||||
|
||||
ItemListingAction.EnterSetupKeyClick -> {
|
||||
sendEvent(ItemListingEvent.NavigateToManualAddItem)
|
||||
}
|
||||
|
||||
ItemListingAction.BackClick -> {
|
||||
sendEvent(ItemListingEvent.NavigateBack)
|
||||
}
|
||||
|
||||
is ItemListingAction.ItemClick -> {
|
||||
sendEvent(ItemListingEvent.NavigateToItem(action.id))
|
||||
}
|
||||
|
||||
ItemListingAction.DialogDismiss -> {
|
||||
handleDialogDismiss()
|
||||
}
|
||||
|
||||
is ItemListingAction.Internal -> {
|
||||
handleInternalAction(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleInternalAction(internalAction: ItemListingAction.Internal) {
|
||||
when (internalAction) {
|
||||
is ItemListingAction.Internal.AuthCodesUpdated -> {
|
||||
handleAuthenticatorDataReceive(internalAction)
|
||||
}
|
||||
|
||||
is ItemListingAction.Internal.AlertThresholdSecondsReceive -> {
|
||||
handleAlertThresholdSecondsReceive(internalAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAlertThresholdSecondsReceive(
|
||||
action: ItemListingAction.Internal.AlertThresholdSecondsReceive,
|
||||
) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
alertThresholdSeconds = action.thresholdSeconds,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -108,72 +107,50 @@ class ItemListingViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleAuthenticatorDataReceive(action: ItemListingAction.Internal.AuthenticatorDataReceive) {
|
||||
when (val viewState = action.itemListingDataState) {
|
||||
is DataState.Error -> authenticatorErrorReceive(viewState)
|
||||
is DataState.Loaded -> authenticatorDataLoadedReceive(viewState.data)
|
||||
private fun handleAuthenticatorDataReceive(
|
||||
action: ItemListingAction.Internal.AuthCodesUpdated,
|
||||
) {
|
||||
updateViewState(action.itemListingDataState)
|
||||
}
|
||||
|
||||
private fun updateViewState(authenticatorData: DataState<List<VerificationCodeItem>>) {
|
||||
when (authenticatorData) {
|
||||
is DataState.Error -> authenticatorErrorReceive(authenticatorData)
|
||||
is DataState.Loaded -> authenticatorDataLoadedReceive(authenticatorData)
|
||||
is DataState.Loading -> authenticatorDataLoadingReceive()
|
||||
is DataState.NoNetwork -> authenticatorNoNetworkReceive(viewState.data)
|
||||
is DataState.Pending -> authenticatorPendingReceive()
|
||||
is DataState.NoNetwork -> authenticatorNoNetworkReceive(authenticatorData)
|
||||
is DataState.Pending -> authenticatorPendingReceive(authenticatorData)
|
||||
}
|
||||
}
|
||||
|
||||
private fun authenticatorErrorReceive(authenticatorData: DataState.Error<ItemListingData>) {
|
||||
mutableStateFlow.update {
|
||||
if (authenticatorData.data != null) {
|
||||
val viewState = updateViewState(authenticatorData.data)
|
||||
|
||||
it.copy(
|
||||
viewState = viewState,
|
||||
dialog = ItemListingState.DialogState.Error(
|
||||
title = R.string.an_error_has_occurred.asText(),
|
||||
message = R.string.generic_error_message.asText(),
|
||||
)
|
||||
)
|
||||
} else {
|
||||
private fun authenticatorErrorReceive(authenticatorData: DataState.Error<List<VerificationCodeItem>>) {
|
||||
if (authenticatorData.data != null) {
|
||||
updateStateWithVerificationCodeItems(
|
||||
authenticatorData = authenticatorData.data,
|
||||
clearDialogState = true
|
||||
)
|
||||
} else {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = ItemListingState.ViewState.Error(
|
||||
R.string.generic_error_message.asText(),
|
||||
)
|
||||
),
|
||||
dialog = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun authenticatorDataLoadedReceive(authenticatorData: ItemListingData) {
|
||||
if (state.dialog == ItemListingState.DialogState.Syncing) {
|
||||
sendEvent(ItemListingEvent.ShowToast(R.string.syncing_complete.asText()))
|
||||
}
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = updateViewState(authenticatorData),
|
||||
dialog = null
|
||||
)
|
||||
}
|
||||
private fun authenticatorDataLoadedReceive(
|
||||
authenticatorData: DataState.Loaded<List<VerificationCodeItem>>,
|
||||
) {
|
||||
updateStateWithVerificationCodeItems(
|
||||
authenticatorData = authenticatorData.data,
|
||||
clearDialogState = true
|
||||
)
|
||||
}
|
||||
|
||||
private fun authenticatorDataLoadingReceive() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = ItemListingState.ViewState.Loading,
|
||||
dialog = ItemListingState.DialogState.Syncing
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun authenticatorNoNetworkReceive(authenticatorData: ItemListingData?) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = updateViewState(authenticatorData),
|
||||
dialog = ItemListingState.DialogState.Error(
|
||||
title = R.string.internet_connection_required_title.asText(),
|
||||
message = R.string.internet_connection_required_message.asText(),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun authenticatorPendingReceive() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = ItemListingState.ViewState.Loading
|
||||
@@ -181,14 +158,48 @@ class ItemListingViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateViewState(
|
||||
itemListingData: ItemListingData?,
|
||||
): ItemListingState.ViewState {
|
||||
val items = itemListingData?.authenticatorData ?: return ItemListingState.ViewState.NoItems
|
||||
return ItemListingState.ViewState.Content(
|
||||
itemList = items.toDisplayItems(itemListingData.alertThresholdSeconds)
|
||||
private fun authenticatorNoNetworkReceive(state: DataState.NoNetwork<List<VerificationCodeItem>>) {
|
||||
if (state.data != null) {
|
||||
updateStateWithVerificationCodeItems(
|
||||
authenticatorData = state.data,
|
||||
clearDialogState = true
|
||||
)
|
||||
} else {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
viewState = ItemListingState.ViewState.Error(
|
||||
message = R.string.internet_connection_required_title
|
||||
.asText()
|
||||
.concat(R.string.internet_connection_required_message.asText())
|
||||
),
|
||||
dialog = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun authenticatorPendingReceive(
|
||||
action: DataState.Pending<List<VerificationCodeItem>>,
|
||||
) {
|
||||
updateStateWithVerificationCodeItems(
|
||||
authenticatorData = action.data,
|
||||
clearDialogState = false
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateStateWithVerificationCodeItems(
|
||||
authenticatorData: List<VerificationCodeItem>,
|
||||
clearDialogState: Boolean,
|
||||
) {
|
||||
mutableStateFlow.update { currentState ->
|
||||
currentState.copy(
|
||||
viewState = authenticatorData.toViewState(
|
||||
alertThresholdSeconds = state.alertThresholdSeconds,
|
||||
),
|
||||
dialog = currentState.dialog.takeUnless { clearDialogState },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -200,6 +211,7 @@ class ItemListingViewModel @Inject constructor(
|
||||
*/
|
||||
@Parcelize
|
||||
data class ItemListingState(
|
||||
val alertThresholdSeconds: Int,
|
||||
val viewState: ViewState,
|
||||
val dialog: DialogState?,
|
||||
) : Parcelable {
|
||||
@@ -346,14 +358,18 @@ sealed class ItemListingAction {
|
||||
/**
|
||||
* Indicates authenticator item listing data has been received.
|
||||
*/
|
||||
data class AuthenticatorDataReceive(
|
||||
val itemListingDataState: DataState<ItemListingData>,
|
||||
data class AuthCodesUpdated(
|
||||
val itemListingDataState: DataState<List<VerificationCodeItem>>,
|
||||
) : Internal()
|
||||
|
||||
data class AlertThresholdSecondsReceive(
|
||||
val thresholdSeconds: Int,
|
||||
) : Internal()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The data for the verification code item to displayed.
|
||||
* The data for the verification code item to display.
|
||||
*/
|
||||
@Parcelize
|
||||
data class VerificationCodeDisplayItem(
|
||||
@@ -366,15 +382,3 @@ data class VerificationCodeDisplayItem(
|
||||
val authCode: String,
|
||||
val startIcon: IconData = IconData.Local(R.drawable.ic_login_item),
|
||||
) : Parcelable
|
||||
|
||||
private fun List<VerificationCodeItem>.toDisplayItems(alertThresholdSeconds: Int) = this.map {
|
||||
VerificationCodeDisplayItem(
|
||||
id = it.id,
|
||||
label = it.name,
|
||||
authCode = it.code,
|
||||
supportingLabel = it.username,
|
||||
periodSeconds = it.periodSeconds,
|
||||
timeLeftSeconds = it.timeLeftSeconds,
|
||||
alertThresholdSeconds = alertThresholdSeconds
|
||||
)
|
||||
}
|
||||
|
||||
-1
@@ -29,5 +29,4 @@ sealed class ItemListingExpandableFabAction(
|
||||
icon,
|
||||
onEnterSetupKeyClick
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.util
|
||||
|
||||
import com.x8bit.bitwarden.authenticator.data.authenticator.manager.model.VerificationCodeItem
|
||||
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.ItemListingState
|
||||
import com.x8bit.bitwarden.authenticator.ui.authenticator.feature.itemlisting.VerificationCodeDisplayItem
|
||||
|
||||
fun List<VerificationCodeItem>.toViewState(
|
||||
alertThresholdSeconds: Int,
|
||||
): ItemListingState.ViewState =
|
||||
if (isEmpty()) {
|
||||
ItemListingState.ViewState.NoItems
|
||||
} else {
|
||||
ItemListingState.ViewState.Content(
|
||||
map { it.toDisplayItem(alertThresholdSeconds = alertThresholdSeconds) }
|
||||
)
|
||||
}
|
||||
|
||||
fun VerificationCodeItem.toDisplayItem(alertThresholdSeconds: Int) =
|
||||
VerificationCodeDisplayItem(
|
||||
id = id,
|
||||
label = name,
|
||||
supportingLabel = username,
|
||||
timeLeftSeconds = timeLeftSeconds,
|
||||
periodSeconds = periodSeconds,
|
||||
alertThresholdSeconds = alertThresholdSeconds,
|
||||
authCode = code,
|
||||
)
|
||||
@@ -0,0 +1,77 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="180dp"
|
||||
android:height="161dp"
|
||||
android:viewportHeight="161"
|
||||
android:viewportWidth="180">
|
||||
<path
|
||||
android:fillColor="#E5E5EA"
|
||||
android:pathData="M134,147H161V154.57C161,156.78 159.21,158.57 157,158.57H138C135.79,158.57 134,156.78 134,154.57V147Z"
|
||||
android:strokeColor="#8E8E93"
|
||||
android:strokeWidth="2" />
|
||||
<path
|
||||
android:fillColor="#E5E5EA"
|
||||
android:pathData="M20,147H47V154.57C47,156.78 45.21,158.57 43,158.57H24C21.79,158.57 20,156.78 20,154.57V147Z"
|
||||
android:strokeColor="#8E8E93"
|
||||
android:strokeWidth="2" />
|
||||
<path
|
||||
android:fillColor="#F2F2F7"
|
||||
android:pathData="M172.38,2H7.62C4.51,2 2,4.52 2,7.62V140.56C2,143.67 4.51,146.18 7.62,146.18H172.38C175.49,146.18 178,143.67 178,140.56V7.62C178,4.52 175.49,2 172.38,2Z"
|
||||
android:strokeColor="#8E8E93"
|
||||
android:strokeWidth="2" />
|
||||
<path
|
||||
android:fillColor="#E5E5EA"
|
||||
android:pathData="M154.44,18H26.56C23.49,18 21,20.49 21,23.56V123.64C21,126.71 23.49,129.2 26.56,129.2H154.44C157.51,129.2 160,126.71 160,123.64V23.56C160,20.49 157.51,18 154.44,18Z"
|
||||
android:strokeColor="#8E8E93"
|
||||
android:strokeWidth="2" />
|
||||
<path
|
||||
android:fillColor="#F2F2F7"
|
||||
android:pathData="M154.76,129.8L49.89,135.92C46.69,136.11 44,133.57 44,130.38V17.55C44,14.36 46.69,11.82 49.89,12.01L154.76,18.13C157.7,18.3 160,20.73 160,23.67V124.26C160,127.2 157.7,129.63 154.76,129.8Z"
|
||||
android:strokeColor="#8E8E93"
|
||||
android:strokeWidth="2" />
|
||||
<path
|
||||
android:fillColor="#F2F2F7"
|
||||
android:pathData="M164.85,40.87C164.85,38.55 162.98,36.68 160.66,36.68C158.34,36.68 156.46,38.55 156.46,40.87V57.63C156.46,59.94 158.34,61.82 160.66,61.82C162.98,61.82 164.85,59.94 164.85,57.63V40.87Z"
|
||||
android:strokeColor="#8E8E93"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:fillColor="#F2F2F7"
|
||||
android:pathData="M164.85,82.77C164.85,80.45 162.98,78.58 160.66,78.58C158.34,78.58 156.46,80.45 156.46,82.77V99.53C156.46,101.84 158.34,103.71 160.66,103.71C162.98,103.71 164.85,101.84 164.85,99.53V82.77Z"
|
||||
android:strokeColor="#8E8E93"
|
||||
android:strokeWidth="1" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M62,45C63.66,45 65,46.26 65,47.81V86.44C65,87.99 63.66,89.25 62,89.25"
|
||||
android:strokeColor="#8E8E93"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeWidth="2" />
|
||||
<path
|
||||
android:fillColor="#8E8E93"
|
||||
android:pathData="M109,86.29C103.24,86.29 98.56,80.74 98.56,73.92C98.56,67.09 103.24,61.55 109,61.55C114.76,61.55 119.42,67.1 119.42,73.92C119.42,80.73 114.75,86.29 109,86.29ZM109,59.83C102.45,59.83 97.12,66.15 97.12,73.92C97.12,81.69 102.45,88.01 109,88.01C115.55,88.01 120.87,81.7 120.87,73.92C120.87,66.14 115.54,59.83 109,59.83Z" />
|
||||
<path
|
||||
android:fillColor="#8E8E93"
|
||||
android:pathData="M107.55,53.53C103.88,53.9 100.54,55.64 97.88,58.31L98.85,59.46C99.13,59.79 99.27,60.23 99.27,60.67C99.27,61.1 99.13,61.54 98.85,61.87C98.58,62.22 98.2,62.38 97.84,62.38C97.47,62.38 97.11,62.22 96.82,61.87L95.86,60.72C93.6,63.87 92.14,67.84 91.84,72.21H93.21C94,72.21 94.65,72.97 94.65,73.91C94.65,74.84 94,75.62 93.21,75.62H91.84C92.14,79.98 93.61,83.96 95.86,87.1L96.82,85.95C97.11,85.61 97.47,85.45 97.84,85.45C98.2,85.45 98.58,85.61 98.85,85.95C99.13,86.28 99.27,86.73 99.27,87.16C99.27,87.59 99.13,88.04 98.85,88.37L97.88,89.52C100.54,92.2 103.89,93.94 107.57,94.29V92.65C107.57,91.71 108.22,90.94 109.01,90.94C101.11,90.94 94.66,83.3 94.66,73.91C94.66,64.52 101.11,56.88 109.01,56.88C108.22,56.88 107.57,56.11 107.57,55.17V53.53M110.45,53.53V55.17C110.45,56.12 109.8,56.88 109.01,56.88C116.92,56.88 123.35,64.52 123.35,73.91C123.35,83.3 116.91,90.94 109.01,90.94C109.8,90.94 110.45,91.72 110.45,92.65V94.29C114.12,93.93 117.47,92.2 120.13,89.52L119.17,88.36C118.88,88.03 118.74,87.58 118.74,87.15C118.74,86.72 118.88,86.27 119.17,85.94C119.43,85.6 119.81,85.44 120.18,85.44C120.54,85.44 120.92,85.6 121.2,85.94L122.17,87.09C124.41,83.93 125.88,79.97 126.19,75.6H124.81C124.01,75.6 123.36,74.84 123.36,73.9C123.36,72.95 124.01,72.2 124.81,72.2H126.19C125.88,67.83 124.41,63.87 122.17,60.71L121.2,61.86C120.92,62.2 120.54,62.36 120.18,62.36C119.81,62.36 119.43,62.2 119.17,61.86C118.88,61.53 118.74,61.09 118.74,60.65C118.74,60.22 118.88,59.78 119.17,59.45L120.13,58.3C117.46,55.62 114.12,53.89 110.45,53.52" />
|
||||
<path
|
||||
android:fillColor="#8E8E93"
|
||||
android:pathData="M109,46C108.21,46 107.55,46.76 107.55,47.71V55.17C107.55,56.12 108.21,56.88 109,56.88C109.79,56.88 110.43,56.11 110.43,55.17V47.71C110.43,46.76 109.8,46 109,46Z" />
|
||||
<path
|
||||
android:fillColor="#8E8E93"
|
||||
android:pathData="M93.38,53.67C93.01,53.67 92.65,53.84 92.36,54.18C91.8,54.84 91.8,55.93 92.36,56.59L95.85,60.72L96.81,61.87C97.09,62.22 97.46,62.37 97.82,62.37C98.19,62.37 98.56,62.22 98.84,61.87C99.12,61.54 99.26,61.1 99.26,60.67C99.26,60.23 99.12,59.79 98.84,59.46L97.87,58.31L94.39,54.18C94.11,53.84 93.73,53.67 93.36,53.67" />
|
||||
<path
|
||||
android:fillColor="#8E8E93"
|
||||
android:pathData="M93.21,72.21H86.92C86.13,72.21 85.47,72.97 85.47,73.91C85.47,74.84 86.13,75.62 86.92,75.62H93.21C94,75.62 94.65,74.85 94.65,73.91C94.65,72.96 94,72.21 93.21,72.21Z" />
|
||||
<path
|
||||
android:fillColor="#8E8E93"
|
||||
android:pathData="M97.84,85.46C97.47,85.46 97.11,85.62 96.82,85.96L95.86,87.11L92.38,91.25C91.81,91.91 91.81,92.99 92.38,93.65C92.66,93.99 93.02,94.16 93.39,94.16C93.75,94.16 94.13,93.99 94.41,93.65L97.88,89.52L98.85,88.37C99.13,88.04 99.27,87.59 99.27,87.16C99.27,86.73 99.13,86.28 98.85,85.95C98.58,85.61 98.2,85.45 97.84,85.45" />
|
||||
<path
|
||||
android:fillColor="#8E8E93"
|
||||
android:pathData="M109,90.95C108.21,90.95 107.55,91.73 107.55,92.66V100.13C107.55,101.07 108.21,101.84 109,101.84C109.79,101.84 110.43,101.07 110.43,100.13V92.66C110.43,91.72 109.79,90.95 109,90.95Z" />
|
||||
<path
|
||||
android:fillColor="#8E8E93"
|
||||
android:pathData="M120.17,85.46C119.8,85.46 119.42,85.62 119.15,85.96C118.87,86.29 118.73,86.74 118.73,87.17C118.73,87.6 118.87,88.05 119.15,88.38L120.12,89.54L123.59,93.67C123.87,94 124.25,94.17 124.61,94.17C124.98,94.17 125.35,94 125.64,93.67C126.2,93.01 126.2,91.92 125.64,91.26L122.15,87.13L121.19,85.97C120.91,85.63 120.53,85.47 120.17,85.47" />
|
||||
<path
|
||||
android:fillColor="#8E8E93"
|
||||
android:pathData="M131.08,72.21H124.8C124,72.21 123.35,72.97 123.35,73.91C123.35,74.84 124,75.62 124.8,75.62H131.08C131.87,75.62 132.53,74.85 132.53,73.91C132.53,72.96 131.87,72.21 131.08,72.21Z" />
|
||||
<path
|
||||
android:fillColor="#8E8E93"
|
||||
android:pathData="M124.61,53.67C124.25,53.67 123.87,53.84 123.59,54.18L120.12,58.31L119.15,59.46C118.87,59.79 118.73,60.23 118.73,60.67C118.73,61.1 118.87,61.54 119.15,61.87C119.42,62.22 119.8,62.37 120.17,62.37C120.53,62.37 120.91,62.22 121.19,61.87L122.15,60.72L125.64,56.59C126.2,55.93 126.2,54.84 125.64,54.18C125.35,53.84 124.98,53.67 124.61,53.67Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M20,5H4C2.9,5 2.01,5.9 2.01,7L2,17C2,18.1 2.9,19 4,19H20C21.1,19 22,18.1 22,17V7C22,5.9 21.1,5 20,5ZM20,7V17H4V7H20ZM13,8H11V10H13V8ZM11,11H13V13H11V11ZM10,8H8V10H10V8ZM8,11H10V13H8V11ZM7,11H5V13H7V11ZM5,8H7V10H5V8ZM16,14H8V16H16V14ZM14,11H16V13H14V11ZM16,8H14V10H16V8ZM17,11H19V13H17V11ZM19,8H17V10H19V8Z" />
|
||||
</vector>
|
||||
@@ -32,4 +32,10 @@
|
||||
<string name="no_thanks">No thanks</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="enable_camer_permission_to_use_the_scanner">Enable camera permission to use the scanner</string>
|
||||
<string name="empty_item_list">Empty Item Listing</string>
|
||||
<string name="you_dont_have_items_to_display">You don\'t have any items to display.</string>
|
||||
<string name="empty_item_list_instruction">Add a new code, sync an existing account, or import codes to secure your accounts.</string>
|
||||
<string name="add_code">Add code</string>
|
||||
<string name="sync_items_with_bitwarden">Sync items from Bitwarden</string>
|
||||
<string name="import_items">Import items</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user