Updated on 2026-08-14
This commit is contained in:
commit
f58f67b7ff
19 changed files with 1659 additions and 1261 deletions
|
|
@ -12,6 +12,7 @@ import com.tangem.tap.common.redux.navigation.FragmentShareTransition
|
|||
import com.tangem.tap.features.addCustomToken.AddCustomTokenFragment
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment
|
||||
import com.tangem.tap.features.details.ui.cardsettings.CardSettingsFragment
|
||||
import com.tangem.tap.features.details.ui.cardsettings.coderecovery.AccessCodeRecoveryFragment
|
||||
import com.tangem.tap.features.details.ui.details.DetailsFragment
|
||||
import com.tangem.tap.features.details.ui.resetcard.ResetCardFragment
|
||||
import com.tangem.tap.features.details.ui.securitymode.SecurityModeFragment
|
||||
|
|
@ -109,6 +110,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
|
|||
AppScreen.CardSettings -> CardSettingsFragment()
|
||||
AppScreen.AppSettings -> AppSettingsFragment()
|
||||
AppScreen.ResetToFactory -> ResetCardFragment()
|
||||
AppScreen.AccessCodeRecovery -> AccessCodeRecoveryFragment()
|
||||
AppScreen.Disclaimer -> DisclaimerFragment()
|
||||
AppScreen.AddTokens -> {
|
||||
val featureToggles = store.state.daggerGraphState.get(
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ enum class AppScreen(
|
|||
OnboardingNote, OnboardingWallet, OnboardingTwins, OnboardingOther,
|
||||
Wallet, WalletDetails,
|
||||
Send,
|
||||
Details, DetailsSecurity, CardSettings, AppSettings, ResetToFactory,
|
||||
Details, DetailsSecurity, CardSettings, AppSettings, ResetToFactory, AccessCodeRecovery,
|
||||
AddTokens, AddCustomToken,
|
||||
WalletConnectSessions,
|
||||
QrScan,
|
||||
|
|
|
|||
|
|
@ -425,7 +425,7 @@ class DetailsMiddleware {
|
|||
fun handle(state: DetailsState, action: DetailsAction.AccessCodeRecovery) {
|
||||
when (action) {
|
||||
is DetailsAction.AccessCodeRecovery.Open -> {
|
||||
// store.dispatch(NavigationAction.NavigateTo(AppScreen.AccessCodeRecovery)) Todo: next PR
|
||||
store.dispatch(NavigationAction.NavigateTo(AppScreen.AccessCodeRecovery))
|
||||
}
|
||||
is DetailsAction.AccessCodeRecovery.SaveChanges -> {
|
||||
scope.launch {
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ fun CardSettings(state: CardSettingsScreenState) {
|
|||
is CardInfo.SignedHashes -> 14.dp
|
||||
is CardInfo.SecurityMode -> 16.dp
|
||||
is CardInfo.ChangeAccessCode -> 16.dp
|
||||
is CardInfo.AccessCodeRecovery -> 16.dp
|
||||
is CardInfo.ResetToFactorySettings -> 28.dp
|
||||
}
|
||||
val paddingTop = when (it) {
|
||||
|
|
@ -128,6 +129,7 @@ fun CardSettings(state: CardSettingsScreenState) {
|
|||
is CardInfo.SignedHashes -> 12.dp
|
||||
is CardInfo.SecurityMode -> 14.dp
|
||||
is CardInfo.ChangeAccessCode -> 16.dp
|
||||
is CardInfo.AccessCodeRecovery -> 16.dp
|
||||
is CardInfo.ResetToFactorySettings -> 16.dp
|
||||
}
|
||||
Column(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import androidx.annotation.StringRes
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.tap.features.details.redux.AccessCodeRecoveryState
|
||||
import com.tangem.tap.features.details.redux.SecurityOption
|
||||
import com.tangem.tap.features.details.ui.securitymode.toTitleRes
|
||||
import com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText
|
||||
|
|
@ -12,6 +13,7 @@ import com.tangem.tap.features.details.redux.CardInfo as ReduxCardInfo
|
|||
|
||||
data class CardSettingsScreenState(
|
||||
val cardDetails: List<CardInfo>? = null,
|
||||
val accessCodeRecoveryState: AccessCodeRecoveryState? = null,
|
||||
val onScanCardClick: () -> Unit,
|
||||
val onElementClick: (CardInfo) -> Unit,
|
||||
)
|
||||
|
|
@ -48,6 +50,16 @@ sealed class CardInfo(
|
|||
clickable = true,
|
||||
)
|
||||
|
||||
class AccessCodeRecovery(val enabled: Boolean) : CardInfo(
|
||||
titleRes = TextReference.Res(R.string.card_settings_access_code_recovery_title),
|
||||
subtitle = if (enabled) {
|
||||
TextReference.Res(R.string.common_enabled)
|
||||
} else {
|
||||
TextReference.Res(R.string.common_disabled)
|
||||
},
|
||||
clickable = true,
|
||||
)
|
||||
|
||||
class ResetToFactorySettings(cardInfo: ReduxCardInfo) : CardInfo(
|
||||
titleRes = TextReference.Res(R.string.card_settings_reset_card_to_factory),
|
||||
subtitle = cardInfo.toResetCardDescriptionText(),
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ class CardSettingsViewModel(private val store: Store<AppState>) {
|
|||
return if (state?.manageSecurityState == null) {
|
||||
CardSettingsScreenState(
|
||||
cardDetails = null,
|
||||
accessCodeRecoveryState = null,
|
||||
onElementClick = {},
|
||||
onScanCardClick = {
|
||||
store.dispatch(DetailsAction.ScanCard)
|
||||
|
|
@ -44,12 +45,15 @@ class CardSettingsViewModel(private val store: Store<AppState>) {
|
|||
if (state.card.backupStatus?.isActive == true && state.card.isAccessCodeSet) {
|
||||
cardDetails.add(CardInfo.ChangeAccessCode)
|
||||
}
|
||||
if (state.accessCodeRecovery != null) {
|
||||
cardDetails.add(CardInfo.AccessCodeRecovery(state.accessCodeRecovery.enabledOnCard))
|
||||
}
|
||||
if (state.resetCardAllowed) {
|
||||
cardDetails.add(CardInfo.ResetToFactorySettings(state.cardInfo))
|
||||
}
|
||||
|
||||
CardSettingsScreenState(
|
||||
cardDetails = cardDetails,
|
||||
accessCodeRecoveryState = state.accessCodeRecovery,
|
||||
onScanCardClick = { },
|
||||
onElementClick = {
|
||||
handleClickingItem(it)
|
||||
|
|
@ -72,6 +76,9 @@ class CardSettingsViewModel(private val store: Store<AppState>) {
|
|||
Analytics.send(Settings.CardSettings.ButtonChangeSecurityMode())
|
||||
store.dispatch(DetailsAction.ManageSecurity.OpenSecurity)
|
||||
}
|
||||
is CardInfo.AccessCodeRecovery -> {
|
||||
store.dispatch(DetailsAction.AccessCodeRecovery.Open)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
package com.tangem.tap.features.details.ui.cardsettings.coderecovery
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.transition.TransitionInflater
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.features.details.redux.DetailsState
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import org.rekotlin.StoreSubscriber
|
||||
|
||||
class AccessCodeRecoveryFragment : Fragment(), StoreSubscriber<DetailsState> {
|
||||
|
||||
private val viewModel = AccessCodeRecoveryViewModel(store)
|
||||
|
||||
private var screenState: MutableState<AccessCodeRecoveryScreenState> =
|
||||
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.accessCodeRecovery))
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val inflater = TransitionInflater.from(requireContext())
|
||||
enterTransition = inflater.inflateTransition(R.transition.fade)
|
||||
exitTransition = inflater.inflateTransition(R.transition.fade)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
return ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
isTransitionGroup = true
|
||||
TangemTheme {
|
||||
AccessCodeRecoveryScreen(
|
||||
state = screenState.value,
|
||||
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
store.subscribe(this) { state ->
|
||||
state.skipRepeats { oldState, newState ->
|
||||
oldState.detailsState == newState.detailsState
|
||||
}.select { it.detailsState }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
store.unsubscribe(this)
|
||||
}
|
||||
|
||||
override fun newState(state: DetailsState) {
|
||||
if (activity == null || view == null) return
|
||||
screenState.value =
|
||||
viewModel.updateState(store.state.detailsState.cardSettingsState?.accessCodeRecovery)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package com.tangem.tap.features.details.ui.cardsettings.coderecovery
|
||||
|
||||
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.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.details.ui.common.DetailsMainButton
|
||||
import com.tangem.tap.features.details.ui.common.DetailsRadioButtonElement
|
||||
import com.tangem.tap.features.details.ui.common.ScreenTitle
|
||||
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
|
||||
import com.tangem.wallet.R
|
||||
|
||||
@Composable
|
||||
fun AccessCodeRecoveryScreen(state: AccessCodeRecoveryScreenState, onBackClick: () -> Unit) {
|
||||
SettingsScreensScaffold(
|
||||
content = { AccessCodeRecoveryOptions(state = state) },
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AccessCodeRecoveryOptions(state: AccessCodeRecoveryScreenState) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(bottom = TangemTheme.dimens.spacing28),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
ScreenTitle(
|
||||
titleRes = R.string.card_settings_access_code_recovery_title,
|
||||
Modifier.padding(bottom = TangemTheme.dimens.spacing36),
|
||||
)
|
||||
|
||||
DetailsRadioButtonElement(
|
||||
title = stringResource(id = R.string.common_enabled),
|
||||
subtitle = stringResource(id = R.string.card_settings_access_code_recovery_enabled_description),
|
||||
selected = state.enabledSelection,
|
||||
onClick = { state.onOptionClick(true) },
|
||||
)
|
||||
DetailsRadioButtonElement(
|
||||
title = stringResource(id = R.string.common_disabled),
|
||||
subtitle = stringResource(id = R.string.card_settings_access_code_recovery_disabled_description),
|
||||
selected = !state.enabledSelection,
|
||||
onClick = { state.onOptionClick(false) },
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
DetailsMainButton(
|
||||
title = stringResource(id = R.string.common_save_changes),
|
||||
enabled = state.isSaveChangesEnabled,
|
||||
onClick = { state.onSaveChangesClick(state.enabledSelection) },
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing20),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.tap.features.details.ui.cardsettings.coderecovery
|
||||
|
||||
/**
|
||||
* @property enabledOnCard Indicates whether access code recovery is enabled on the card
|
||||
* @property enabledSelection Represents the currently selected option in the app (not yet saved on the card)
|
||||
* @property isSaveChangesEnabled Determines if the user is allowed to save their selection to the card
|
||||
* @property onSaveChangesClick Callback function called when the user wants to apply the selected option
|
||||
* @property onOptionClick Callback function called when the user selects an option
|
||||
* */
|
||||
data class AccessCodeRecoveryScreenState(
|
||||
val enabledOnCard: Boolean,
|
||||
val enabledSelection: Boolean,
|
||||
val isSaveChangesEnabled: Boolean,
|
||||
val onSaveChangesClick: (Boolean) -> Unit,
|
||||
val onOptionClick: (Boolean) -> Unit,
|
||||
)
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.tap.features.details.ui.cardsettings.coderecovery
|
||||
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.details.redux.AccessCodeRecoveryState
|
||||
import com.tangem.tap.features.details.redux.DetailsAction
|
||||
import org.rekotlin.Store
|
||||
|
||||
class AccessCodeRecoveryViewModel(val store: Store<AppState>) {
|
||||
|
||||
fun updateState(state: AccessCodeRecoveryState?): AccessCodeRecoveryScreenState {
|
||||
// We shouldn't get to this screen here when this state is null
|
||||
return if (state == null) {
|
||||
AccessCodeRecoveryScreenState(
|
||||
enabledOnCard = false,
|
||||
enabledSelection = false,
|
||||
isSaveChangesEnabled = false,
|
||||
onSaveChangesClick = {},
|
||||
onOptionClick = {},
|
||||
)
|
||||
} else {
|
||||
AccessCodeRecoveryScreenState(
|
||||
enabledOnCard = state.enabledOnCard,
|
||||
enabledSelection = state.enabledSelection,
|
||||
isSaveChangesEnabled = state.enabledOnCard != state.enabledSelection,
|
||||
onSaveChangesClick = { store.dispatch(DetailsAction.AccessCodeRecovery.SaveChanges(it)) },
|
||||
onOptionClick = { store.dispatch(DetailsAction.AccessCodeRecovery.SelectOption(it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,18 +3,18 @@ package com.tangem.tap.features.details.ui.common
|
|||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.RadioButton
|
||||
import androidx.compose.material.RadioButtonDefaults
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
|
|
@ -25,7 +25,9 @@ import androidx.compose.ui.res.colorResource
|
|||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.PrimaryButtonIconRight
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.common.compose.TangemTypography
|
||||
import com.tangem.wallet.R
|
||||
|
||||
@Composable
|
||||
|
|
@ -109,26 +111,49 @@ fun EmptyTopBarWithNavigation(
|
|||
|
||||
@Composable
|
||||
fun DetailsMainButton(title: String, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true) {
|
||||
Button(
|
||||
PrimaryButtonIconRight(
|
||||
text = title,
|
||||
enabled = enabled,
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
.fillMaxWidth(),
|
||||
icon = painterResource(id = R.drawable.ic_tangem_24),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DetailsRadioButtonElement(title: String, subtitle: String, selected: Boolean, onClick: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(48.dp),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
enabled = enabled,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
backgroundColor = colorResource(R.color.button_primary),
|
||||
contentColor = colorResource(R.color.text_primary_2),
|
||||
disabledBackgroundColor = colorResource(R.color.button_disabled),
|
||||
disabledContentColor = colorResource(R.color.text_disabled),
|
||||
),
|
||||
.selectable(
|
||||
selected = selected,
|
||||
onClick = { onClick() },
|
||||
)
|
||||
.padding(start = 20.dp, end = 20.dp, top = 16.dp, bottom = 16.dp),
|
||||
) {
|
||||
Text(text = title)
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.padding(start = 20.dp, end = 20.dp)
|
||||
.size(8.dp),
|
||||
RadioButton(
|
||||
selected = selected,
|
||||
onClick = null,
|
||||
modifier = Modifier.padding(end = 20.dp),
|
||||
colors = RadioButtonDefaults.colors(
|
||||
unselectedColor = colorResource(id = R.color.icon_secondary),
|
||||
selectedColor = colorResource(id = R.color.icon_accent),
|
||||
),
|
||||
)
|
||||
Icon(painter = painterResource(id = R.drawable.ic_tangem_24), contentDescription = "")
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = title,
|
||||
style = TangemTypography.subtitle1,
|
||||
color = colorResource(id = R.color.text_primary_1),
|
||||
)
|
||||
Spacer(modifier = Modifier.size(4.dp))
|
||||
Text(
|
||||
text = subtitle,
|
||||
style = TangemTypography.body2,
|
||||
color = colorResource(id = R.color.text_secondary),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,27 +2,19 @@ package com.tangem.tap.features.details.ui.securitymode
|
|||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.RadioButton
|
||||
import androidx.compose.material.RadioButtonDefaults
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.tap.common.compose.TangemTypography
|
||||
import com.tangem.tap.features.details.redux.SecurityOption
|
||||
import com.tangem.tap.features.details.ui.common.DetailsMainButton
|
||||
import com.tangem.tap.features.details.ui.common.DetailsRadioButtonElement
|
||||
import com.tangem.tap.features.details.ui.common.ScreenTitle
|
||||
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
|
||||
import com.tangem.wallet.R
|
||||
|
|
@ -74,39 +66,12 @@ fun SecurityOption(option: SecurityOption, state: SecurityModeScreenState) {
|
|||
SecurityOption.AccessCode -> R.string.details_manage_security_access_code_description
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.selectable(
|
||||
selected = selected,
|
||||
onClick = { state.onNewModeSelected(option) },
|
||||
)
|
||||
.padding(start = 20.dp, end = 20.dp, top = 16.dp, bottom = 16.dp),
|
||||
) {
|
||||
RadioButton(
|
||||
selected = selected,
|
||||
onClick = null,
|
||||
modifier = Modifier.padding(end = 20.dp),
|
||||
colors = RadioButtonDefaults.colors(
|
||||
unselectedColor = colorResource(id = R.color.icon_secondary),
|
||||
selectedColor = colorResource(id = R.color.icon_accent),
|
||||
),
|
||||
)
|
||||
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(id = title),
|
||||
style = TangemTypography.subtitle1,
|
||||
color = colorResource(id = R.color.text_primary_1),
|
||||
)
|
||||
Spacer(modifier = Modifier.size(4.dp))
|
||||
Text(
|
||||
text = stringResource(id = subtitle),
|
||||
style = TangemTypography.body2,
|
||||
color = colorResource(id = R.color.text_secondary),
|
||||
)
|
||||
}
|
||||
}
|
||||
DetailsRadioButtonElement(
|
||||
title = stringResource(id = title),
|
||||
subtitle = stringResource(id = subtitle),
|
||||
selected = selected,
|
||||
onClick = { state.onNewModeSelected(option) },
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue