Updated on 2026-08-14
This commit is contained in:
parent
102f4cf536
commit
a370de2bcb
19 changed files with 110 additions and 648 deletions
|
|
@ -77,9 +77,9 @@ dependencies {
|
|||
implementation 'com.google.android.play:core-ktx:1.8.1'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
|
||||
implementation 'com.tangem:blockchain:develop-53'
|
||||
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-115'
|
||||
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-115'
|
||||
implementation 'com.tangem:blockchain:BSK-107_update_sdk-54'
|
||||
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-125'
|
||||
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-125'
|
||||
|
||||
// WebView
|
||||
implementation "androidx.browser:browser:1.3.0"
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit e80938f7d9ba378a91ac544a9170597e64afa696
|
||||
Subproject commit 42803296690ad6914ec2c59c5fdc3f0fb30f8afc
|
||||
|
|
@ -105,6 +105,7 @@ object TangemSdk {
|
|||
is TangemSdkError.WrongInteractionMode -> TangemSdkError.WrongInteractionMode()
|
||||
is TangemSdkError.IssuerSignatureLoadingFailed -> TangemSdkError.IssuerSignatureLoadingFailed()
|
||||
is TangemSdkError.BackupFailedFirmware -> TangemSdkError.BackupFailedFirmware()
|
||||
is TangemSdkError.UserForgotTheCode -> TangemSdkError.UserForgotTheCode()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
|
||||
data class ByteArrayKey(val bytes: ByteArray) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return this === other || other is ByteArrayKey && this.bytes.contentEquals(other.bytes)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = bytes.contentHashCode()
|
||||
}
|
||||
|
||||
fun ByteArray.toMapKey(): ByteArrayKey = ByteArrayKey(this)
|
||||
|
||||
fun ByteArrayKey.bytes(): ByteArray = this.bytes
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
package com.tangem.tap.common.leapfrogWidget
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.ValueAnimator
|
||||
import android.view.View
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.view.animation.LinearInterpolator
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
//fun View.setForeground(hasForegroundAtEnd: Boolean) {
|
||||
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// foreground = getForegroundDrawable()
|
||||
// }
|
||||
//}
|
||||
|
||||
//fun View.getForegroundDrawable(): Drawable? {
|
||||
// return AppCompatResources.getDrawable(context, R.drawable.shape_rectangle_rounded_8_op_20)?.also {
|
||||
// it.alpha = 60
|
||||
// }
|
||||
//}
|
||||
|
||||
typealias ProgressListener = (Int) -> Unit
|
||||
|
||||
fun createProgressListener(duration: Long, listener: ProgressListener?): Animator {
|
||||
val valueAnimator = ValueAnimator.ofInt(0, 100)
|
||||
valueAnimator.duration = duration
|
||||
valueAnimator.addUpdateListener { listener?.invoke((it.animatedValue as? Int) ?: 0) }
|
||||
return valueAnimator
|
||||
}
|
||||
|
||||
fun View.unfoldAnimation(animDuration: Long, properties: LeapViewProperties): AnimatorSet {
|
||||
val translate = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslation)
|
||||
translate.duration = animDuration
|
||||
return AnimatorSet().apply { playTogether(translate) }
|
||||
}
|
||||
|
||||
fun View.foldAnimation(animDuration: Long, properties: LeapViewProperties): AnimatorSet {
|
||||
val translate = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslation)
|
||||
translate.duration = animDuration
|
||||
|
||||
return AnimatorSet().apply {
|
||||
playSequentially(translate)
|
||||
}
|
||||
}
|
||||
|
||||
fun View.leapAnimation(animDuration: Long, properties: LeapViewProperties, overLift: Float): AnimatorSet {
|
||||
val halfDuration = animDuration / 2
|
||||
|
||||
val upTo = (height.toFloat() + overLift) * -1
|
||||
val translateUp = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, upTo)
|
||||
translateUp.duration = halfDuration
|
||||
translateUp.interpolator = LinearInterpolator()
|
||||
|
||||
val scaleDuration = halfDuration - halfDuration / 5
|
||||
val scaleX = ObjectAnimator.ofFloat(this, View.SCALE_X, properties.scale)
|
||||
val scaleY = ObjectAnimator.ofFloat(this, View.SCALE_Y, properties.scale)
|
||||
val scale = AnimatorSet().apply {
|
||||
startDelay = scaleDuration
|
||||
duration = scaleDuration
|
||||
interpolator = LinearInterpolator()
|
||||
playTogether(scaleX, scaleY)
|
||||
}
|
||||
|
||||
val elevation = elevationAnimator(properties.elevationStart, properties.elevationEnd, halfDuration, 100)
|
||||
val translateDown = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslation)
|
||||
translateDown.startDelay = halfDuration
|
||||
translateDown.duration = halfDuration
|
||||
translateDown.interpolator = LinearInterpolator()
|
||||
|
||||
return AnimatorSet().apply {
|
||||
playTogether(translateUp, scale, elevation, translateDown)
|
||||
}
|
||||
}
|
||||
|
||||
fun View.leapBackAnimation(animDuration: Long, properties: LeapViewProperties, calculator: PropertyCalculator): AnimatorSet {
|
||||
val halfDuration = animDuration / 2
|
||||
|
||||
val upTo = ((height).toFloat() - calculator.yTranslationFactor) * -1
|
||||
val translateUp = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, upTo)
|
||||
translateUp.duration = halfDuration
|
||||
translateUp.interpolator = LinearInterpolator()
|
||||
|
||||
val scaleDuration = halfDuration - halfDuration / 5
|
||||
val scaleX = ObjectAnimator.ofFloat(this, View.SCALE_X, properties.scale)
|
||||
val scaleY = ObjectAnimator.ofFloat(this, View.SCALE_Y, properties.scale)
|
||||
val scale = AnimatorSet().apply {
|
||||
startDelay = scaleDuration
|
||||
duration = scaleDuration
|
||||
interpolator = LinearInterpolator()
|
||||
playTogether(scaleX, scaleY)
|
||||
}
|
||||
|
||||
val elevation = elevationAnimator(properties.elevationStart, properties.elevationEnd, halfDuration, 100)
|
||||
val translateDown = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslation)
|
||||
translateDown.startDelay = halfDuration
|
||||
translateDown.duration = halfDuration
|
||||
|
||||
return AnimatorSet().apply {
|
||||
playTogether(translateUp, scale, elevation, translateDown)
|
||||
}
|
||||
}
|
||||
|
||||
fun View.pullUpAnimation(leapDuration: Long, properties: LeapViewProperties): AnimatorSet {
|
||||
val pullDuration = leapDuration / 2
|
||||
val delayDuration = leapDuration / 2
|
||||
|
||||
val translateUp = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslation)
|
||||
val scaleX = ObjectAnimator.ofFloat(this, View.SCALE_X, properties.scale)
|
||||
val scaleY = ObjectAnimator.ofFloat(this, View.SCALE_Y, properties.scale)
|
||||
val elevation = elevationAnimator(properties.elevationStart, properties.elevationEnd)
|
||||
|
||||
return AnimatorSet().apply {
|
||||
startDelay = delayDuration
|
||||
duration = pullDuration
|
||||
interpolator = DecelerateInterpolator()
|
||||
playTogether(translateUp, scaleX, scaleY, elevation)
|
||||
}
|
||||
}
|
||||
|
||||
fun View.pullDownAnimation(leapDuration: Long, properties: LeapViewProperties): AnimatorSet {
|
||||
val pullDuration = leapDuration / 2
|
||||
val delayDuration = leapDuration / 4
|
||||
|
||||
val translate = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslation)
|
||||
val scaleX = ObjectAnimator.ofFloat(this, View.SCALE_X, properties.scale)
|
||||
val scaleY = ObjectAnimator.ofFloat(this, View.SCALE_Y, properties.scale)
|
||||
val elevation = elevationAnimator(properties.elevationStart, properties.elevationEnd)
|
||||
|
||||
return AnimatorSet().apply {
|
||||
startDelay = delayDuration
|
||||
duration = pullDuration
|
||||
interpolator = DecelerateInterpolator()
|
||||
playTogether(translate, scaleX, scaleY, elevation)
|
||||
}
|
||||
}
|
||||
|
||||
fun View.elevationAnimator(start: Float, end: Float, startDelay: Long? = null, duration: Long? = null): Animator {
|
||||
val elevationAnimator = ValueAnimator.ofFloat(start, end)
|
||||
elevationAnimator.addUpdateListener { this.elevation = (it.animatedValue as? Float) ?: start }
|
||||
startDelay?.let { elevationAnimator.startDelay = it }
|
||||
duration?.let { elevationAnimator.duration = it }
|
||||
return elevationAnimator
|
||||
}
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
package com.tangem.tap.common.leapfrogWidget
|
||||
|
||||
import android.view.View
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class LeapView(
|
||||
val view: View,
|
||||
val index: Int,
|
||||
position: Int,
|
||||
private val maximalPosition: Int,
|
||||
private val calculator: PropertyCalculator,
|
||||
) {
|
||||
var state: LeapViewState
|
||||
private set
|
||||
|
||||
val initialState: LeapViewState
|
||||
|
||||
init {
|
||||
val initialProperties = createInitialProperty(position)
|
||||
state = LeapViewState(index, position, position, initialProperties)
|
||||
initialState = state.copy()
|
||||
}
|
||||
|
||||
fun initView() {
|
||||
applyState(initialState)
|
||||
}
|
||||
|
||||
fun leap(): LeapFrogAnimation {
|
||||
val previousPosition = state.currentPosition
|
||||
val currentPosition = when (previousPosition) {
|
||||
0 -> maximalPosition
|
||||
else -> state.currentPosition - 1
|
||||
}
|
||||
|
||||
state = state.copy(
|
||||
currentPosition = currentPosition,
|
||||
previousPosition = previousPosition,
|
||||
properties = createLeapAnimationProperty(previousPosition, currentPosition)
|
||||
)
|
||||
|
||||
return when {
|
||||
previousPosition == 0 && currentPosition == maximalPosition -> LeapFrogAnimation.LEAP
|
||||
else -> LeapFrogAnimation.PULL
|
||||
}
|
||||
}
|
||||
|
||||
fun leapBack(): LeapFrogAnimation {
|
||||
val previousPosition = state.currentPosition
|
||||
val currentPosition = when (previousPosition) {
|
||||
maximalPosition -> 0
|
||||
else -> state.currentPosition + 1
|
||||
}
|
||||
state = state.copy(
|
||||
currentPosition = currentPosition,
|
||||
previousPosition = previousPosition,
|
||||
properties = createLeapAnimationProperty(previousPosition, currentPosition)
|
||||
)
|
||||
|
||||
return when {
|
||||
previousPosition == maximalPosition && currentPosition == 0 -> LeapFrogAnimation.LEAP
|
||||
else -> LeapFrogAnimation.PULL
|
||||
}
|
||||
}
|
||||
|
||||
fun fold() {
|
||||
state = state.copy(properties = state.properties.toFold()
|
||||
)
|
||||
}
|
||||
|
||||
fun unfold() {
|
||||
state = state.copy(properties = state.properties.toUnfold(calculator))
|
||||
}
|
||||
|
||||
fun applyState(state: LeapViewState) {
|
||||
this.state = state
|
||||
setProperties(this.state.properties)
|
||||
}
|
||||
|
||||
private fun setProperties(properties: LeapViewProperties) {
|
||||
view.elevation = properties.elevationEnd
|
||||
view.translationY = properties.yTranslation
|
||||
view.scaleX = properties.scale
|
||||
view.scaleY = properties.scale
|
||||
// initialProperties.hasForeground
|
||||
}
|
||||
|
||||
private fun createInitialProperty(endPosition: Int): LeapViewProperties {
|
||||
return LeapViewProperties(
|
||||
endPosition,
|
||||
endPosition,
|
||||
calculator.scale(endPosition),
|
||||
calculator.elevation(endPosition, maximalPosition + 1),
|
||||
calculator.elevation(endPosition, maximalPosition + 1),
|
||||
calculator.yTranslation(0),
|
||||
calculator.hasForeground(endPosition),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createLeapAnimationProperty(startPosition: Int, endPosition: Int): LeapViewProperties {
|
||||
return LeapViewProperties(
|
||||
startPosition,
|
||||
endPosition,
|
||||
calculator.scale(endPosition),
|
||||
calculator.elevation(startPosition, maximalPosition + 1),
|
||||
calculator.elevation(endPosition, maximalPosition + 1),
|
||||
calculator.yTranslation(endPosition),
|
||||
calculator.hasForeground(endPosition),
|
||||
)
|
||||
}
|
||||
|
||||
private fun LeapViewProperties.toUnfold(calculator: PropertyCalculator): LeapViewProperties {
|
||||
return this.copy(yTranslation = calculator.yTranslation(this.positionEnd))
|
||||
}
|
||||
|
||||
private fun LeapViewProperties.toFold(): LeapViewProperties {
|
||||
return this.copy(yTranslation = 0f)
|
||||
}
|
||||
|
||||
override fun toString(): String = "index: $index, position: ${state.currentPosition}, code: ${view.hashCode()}"
|
||||
|
||||
}
|
||||
|
||||
data class LeapViewState(
|
||||
val index: Int,
|
||||
val currentPosition: Int,
|
||||
val previousPosition: Int,
|
||||
val properties: LeapViewProperties
|
||||
)
|
||||
|
||||
enum class LeapFrogAnimation {
|
||||
LEAP, PULL
|
||||
}
|
||||
|
||||
data class LeapViewProperties(
|
||||
val positionStart: Int,
|
||||
val positionEnd: Int,
|
||||
val scale: Float,
|
||||
val elevationStart: Float,
|
||||
val elevationEnd: Float,
|
||||
val yTranslation: Float,
|
||||
val hasForeground: Boolean,
|
||||
)
|
||||
|
|
@ -1,249 +0,0 @@
|
|||
package com.tangem.tap.common.leapfrogWidget
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorSet
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.core.view.children
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.tangem_sdk_new.extensions.dpToPx
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class LeapfrogWidget(
|
||||
private val parentContainer: FrameLayout,
|
||||
private val calculator: PropertyCalculator = PropertyCalculator(),
|
||||
) {
|
||||
private val foldAnimationDuration = 300L
|
||||
private val leapAnimationDuration = 500L
|
||||
private val leapBackAnimationDuration = 500L
|
||||
|
||||
private val leapViews = mutableListOf<LeapView>()
|
||||
|
||||
private var foldUnfoldProgress: Int = 100
|
||||
private var leapProgress: Int = 100
|
||||
private var leapBackProgress: Int = 100
|
||||
|
||||
init {
|
||||
calculator.setTranslationConverter { parentContainer.dpToPx(it) }
|
||||
if (viewIsFullFledged()) {
|
||||
val maxPosition = parentContainer.childCount - 1
|
||||
parentContainer.children.forEachIndexed { index, view ->
|
||||
val initialPosition = maxPosition - index
|
||||
leapViews.add(LeapView(view, index, initialPosition, maxPosition, calculator))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply initial properties to the views
|
||||
*/
|
||||
fun initViews() {
|
||||
leapViews.forEach { it.initView() }
|
||||
}
|
||||
|
||||
fun fold(animate: Boolean = true, onEnd: VoidCallback = {}) {
|
||||
val animator = foldAnimator(animate)
|
||||
if (animator == null) {
|
||||
onEnd()
|
||||
} else {
|
||||
animator.doOnEnd { onEnd() }
|
||||
animator.start()
|
||||
}
|
||||
}
|
||||
|
||||
fun unfold(animate: Boolean = true, onEnd: VoidCallback = {}) {
|
||||
val animator = unfoldAnimator(animate)
|
||||
if (animator == null) {
|
||||
onEnd()
|
||||
} else {
|
||||
animator.doOnEnd { onEnd() }
|
||||
animator.start()
|
||||
}
|
||||
}
|
||||
|
||||
fun foldAnimator(animate: Boolean = true): Animator? {
|
||||
if (!canFoldUnfold()) return null
|
||||
|
||||
return createFoldUnfoldAnimator(true, animate, foldAnimationDuration)
|
||||
}
|
||||
|
||||
fun unfoldAnimator(animate: Boolean = true): Animator? {
|
||||
if (!canFoldUnfold()) return null
|
||||
|
||||
return createFoldUnfoldAnimator(false, animate, foldAnimationDuration)
|
||||
}
|
||||
|
||||
private fun createFoldUnfoldAnimator(isFoldAnimation: Boolean, animate: Boolean, duration: Long): Animator {
|
||||
val animatorsList = mutableListOf<Animator>()
|
||||
val animateDuration = if (animate) duration else 0
|
||||
leapViews.forEach {
|
||||
if (isFoldAnimation) {
|
||||
it.fold()
|
||||
animatorsList.add(it.view.foldAnimation(animateDuration, it.state.properties))
|
||||
} else {
|
||||
it.unfold()
|
||||
animatorsList.add(it.view.unfoldAnimation(animateDuration, it.state.properties))
|
||||
}
|
||||
}
|
||||
animatorsList.add(createProgressListener(animateDuration) { foldUnfoldProgress = it })
|
||||
|
||||
val animator = AnimatorSet()
|
||||
animator.interpolator = AccelerateDecelerateInterpolator()
|
||||
animator.playTogether(animatorsList.toList())
|
||||
return animator
|
||||
}
|
||||
|
||||
fun leap(animate: Boolean = true, onEnd: VoidCallback = {}) {
|
||||
if (!canLeap()) {
|
||||
onEnd()
|
||||
return
|
||||
}
|
||||
|
||||
val duration = if (animate) leapAnimationDuration else 0
|
||||
val animatorsList = mutableListOf<Animator>()
|
||||
leapViews.forEach {
|
||||
when (it.leap()) {
|
||||
LeapFrogAnimation.LEAP -> {
|
||||
val overLift = calculator.overLift(leapViews.size)
|
||||
animatorsList.add(it.view.leapAnimation(duration, it.state.properties, overLift))
|
||||
}
|
||||
LeapFrogAnimation.PULL -> {
|
||||
animatorsList.add(it.view.pullUpAnimation(duration, it.state.properties))
|
||||
}
|
||||
}
|
||||
}
|
||||
animatorsList.add(createProgressListener(duration) { leapProgress = it })
|
||||
|
||||
val animator = AnimatorSet()
|
||||
animator.playTogether(animatorsList.toList())
|
||||
animator.doOnEnd { onEnd() }
|
||||
animator.start()
|
||||
}
|
||||
|
||||
fun leapBack(animate: Boolean = true, onEnd: VoidCallback = {}) {
|
||||
if (!canLeapBack()) {
|
||||
onEnd()
|
||||
return
|
||||
}
|
||||
|
||||
val duration = if (animate) leapBackAnimationDuration else 0
|
||||
val animatorsList = mutableListOf<Animator>()
|
||||
leapViews.forEach {
|
||||
when (it.leapBack()) {
|
||||
LeapFrogAnimation.LEAP -> {
|
||||
animatorsList.add(it.view.leapBackAnimation(duration, it.state.properties, calculator))
|
||||
}
|
||||
LeapFrogAnimation.PULL -> {
|
||||
animatorsList.add(it.view.pullDownAnimation(duration, it.state.properties))
|
||||
}
|
||||
}
|
||||
}
|
||||
animatorsList.add(createProgressListener(duration) { leapBackProgress = it })
|
||||
|
||||
val animator = AnimatorSet()
|
||||
animator.playTogether(animatorsList.toList())
|
||||
animator.doOnEnd { onEnd() }
|
||||
animator.start()
|
||||
}
|
||||
|
||||
fun getViewsCount(): Int = parentContainer.childCount
|
||||
|
||||
fun getViewPositionByIndex(index: Int): Int = leapViews.first { it.index == index }.state.currentPosition
|
||||
|
||||
fun getViewByPosition(position: Int): LeapView = leapViews.first { it.state.currentPosition == position }
|
||||
|
||||
fun getState(): LeapfrogWidgetState = LeapfrogWidgetState(leapViews.map { it.state })
|
||||
|
||||
fun applyState(state: LeapfrogWidgetState) {
|
||||
state.leapViewStates.forEach { viewState ->
|
||||
leapViews.firstOrNull { it.index == viewState.index }?.applyState(viewState)
|
||||
}
|
||||
}
|
||||
|
||||
private fun viewIsFullFledged(): Boolean = parentContainer.childCount > 1
|
||||
|
||||
private fun canFoldUnfold(): Boolean {
|
||||
return when {
|
||||
!viewIsFullFledged() -> false
|
||||
foldUnfoldInProgress() -> false
|
||||
leapInProgress() || leapBackInProgress() -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
private fun canLeap(): Boolean {
|
||||
return when {
|
||||
!viewIsFullFledged() -> false
|
||||
leapBackInProgress() -> false
|
||||
leapProgress < 70 -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
private fun canLeapBack(): Boolean {
|
||||
return when {
|
||||
!viewIsFullFledged() -> false
|
||||
leapInProgress() -> false
|
||||
leapBackProgress < 70 -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
private fun foldUnfoldInProgress(): Boolean = foldUnfoldProgress != 100
|
||||
private fun leapInProgress(): Boolean = leapProgress != 100
|
||||
private fun leapBackInProgress(): Boolean = leapBackProgress != 100
|
||||
}
|
||||
|
||||
data class LeapfrogWidgetState(
|
||||
val leapViewStates: List<LeapViewState>
|
||||
)
|
||||
|
||||
class PropertyCalculator(
|
||||
val elevationFactor: Float = 1f,
|
||||
val decreaseScaleFactor: Float = 0.15f,
|
||||
val yTranslationFactor: Float = 35f,
|
||||
val decreaseOverLiftingFactor: Float = 0.85f,
|
||||
) {
|
||||
// 0 view it is view on the top of stack, but it is last in parent.children
|
||||
|
||||
private var translationValueConverter: ((Float) -> Float)? = null
|
||||
fun setTranslationConverter(converter: (Float) -> Float) {
|
||||
translationValueConverter = converter
|
||||
}
|
||||
|
||||
fun scale(position: Int): Float {
|
||||
return 1f - decreaseScaleFactor * position
|
||||
}
|
||||
|
||||
fun elevation(position: Int, count: Int): Float {
|
||||
return elevationFactor * (count - 1 - position)
|
||||
}
|
||||
|
||||
fun yTranslation(position: Int): Float {
|
||||
val yTranslation = yTranslationFactor * position
|
||||
return convertTranslation(yTranslation)
|
||||
}
|
||||
|
||||
fun hasForeground(position: Int): Boolean {
|
||||
return position != 0
|
||||
}
|
||||
|
||||
fun overLift(viewCount: Int): Float {
|
||||
val initialOverLift = when {
|
||||
yTranslationFactor < 0f -> yTranslationFactor * decreaseOverLiftingFactor * viewCount * -1
|
||||
else -> 10f
|
||||
}
|
||||
val scaleOverLift = when {
|
||||
decreaseScaleFactor == 0f -> initialOverLift
|
||||
else -> initialOverLift * decreaseOverLiftingFactor - initialOverLift * decreaseScaleFactor
|
||||
}
|
||||
return convertTranslation(scaleOverLift)
|
||||
}
|
||||
|
||||
private fun convertTranslation(value: Float): Float {
|
||||
return translationValueConverter?.invoke(value) ?: value
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@ import android.view.View
|
|||
import android.widget.FrameLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.transition.TransitionInflater
|
||||
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
|
||||
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidgetState
|
||||
import com.tangem.tap.domain.twins.TwinsCardWidget
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.android.synthetic.main.test_leapfrog_fragment.*
|
||||
|
|
|
|||
|
|
@ -14,18 +14,18 @@ import com.tangem.common.core.CardIdDisplayFormat
|
|||
import com.tangem.common.core.CardSessionRunnable
|
||||
import com.tangem.common.core.Config
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.hdWallet.DerivationPath
|
||||
import com.tangem.operations.CommandResponse
|
||||
import com.tangem.operations.derivation.DerivationTaskResponse
|
||||
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
|
||||
import com.tangem.operations.pins.CheckUserCodesCommand
|
||||
import com.tangem.operations.pins.CheckUserCodesResponse
|
||||
import com.tangem.operations.pins.SetUserCodeCommand
|
||||
import com.tangem.tap.common.analytics.AnalyticsEvent
|
||||
import com.tangem.tap.common.analytics.AnalyticsHandler
|
||||
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
|
||||
import com.tangem.tap.common.extensions.ByteArrayKey
|
||||
import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask
|
||||
import com.tangem.tap.domain.tasks.DerivationTask
|
||||
import com.tangem.tap.domain.tasks.DerivationTaskResponse
|
||||
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
|
||||
import com.tangem.tap.domain.tasks.product.ScanProductTask
|
||||
import com.tangem.tap.domain.tasks.product.ScanResponse
|
||||
|
|
@ -81,7 +81,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
|
|||
cardId: String,
|
||||
derivations: Map<ByteArrayKey, List<DerivationPath>>
|
||||
): CompletionResult<DerivationTaskResponse> {
|
||||
return runTaskAsyncReturnOnMain(DerivationTask(derivations), cardId)
|
||||
return runTaskAsyncReturnOnMain(DeriveMultipleWalletPublicKeysTask(derivations), cardId)
|
||||
}
|
||||
|
||||
suspend fun resetToFactorySettings(card: Card): CompletionResult<Card> {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import com.tangem.blockchain.common.WalletManagerFactory
|
|||
import com.tangem.common.card.CardWallet
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.extensions.hexToBytes
|
||||
import com.tangem.tap.common.extensions.toMapKey
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.tap.domain.TapWorkarounds.isTestCard
|
||||
import com.tangem.tap.domain.tasks.product.ScanResponse
|
||||
|
||||
|
|
@ -33,13 +33,23 @@ fun WalletManagerFactory.makeWalletManagerForApp(
|
|||
}
|
||||
seedKey != null -> {
|
||||
val derivedKeys = scanResponse.derivedKeys[wallet.publicKey.toMapKey()]
|
||||
val derivedKey = derivedKeys?.firstOrNull { it.derivationPath == blockchain.derivationPath() }
|
||||
val derivedKey = derivedKeys?.get(blockchain.derivationPath())
|
||||
?: return null
|
||||
|
||||
makeWalletManager(card.cardId, environmentBlockchain, seedKey, derivedKey)
|
||||
makeWalletManager(
|
||||
cardId = card.cardId,
|
||||
blockchain = environmentBlockchain,
|
||||
seedKey = wallet.publicKey,
|
||||
derivedKey = derivedKey
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
makeWalletManager(card.cardId, environmentBlockchain, wallet.publicKey, wallet.curve)
|
||||
makeWalletManager(
|
||||
cardId = card.cardId,
|
||||
blockchain = environmentBlockchain,
|
||||
walletPublicKey = wallet.publicKey,
|
||||
curve = wallet.curve
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,50 +1,55 @@
|
|||
package com.tangem.tap.domain.tasks
|
||||
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.CardSession
|
||||
import com.tangem.common.core.CardSessionRunnable
|
||||
import com.tangem.common.core.CompletionCallback
|
||||
import com.tangem.common.hdWallet.DerivationPath
|
||||
import com.tangem.operations.CommandResponse
|
||||
import com.tangem.operations.derivation.DeriveWalletPublicKeysTask
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeyList
|
||||
import com.tangem.tap.common.extensions.ByteArrayKey
|
||||
|
||||
class DerivationTaskResponse(
|
||||
val entries: Map<ByteArrayKey, ExtendedPublicKeyList>
|
||||
): CommandResponse
|
||||
|
||||
class DerivationTask(
|
||||
private val derivations: Map<ByteArrayKey, List<DerivationPath>>
|
||||
) : CardSessionRunnable<DerivationTaskResponse> {
|
||||
|
||||
val response: MutableMap<ByteArrayKey, ExtendedPublicKeyList> = mutableMapOf()
|
||||
|
||||
override fun run(session: CardSession, callback: CompletionCallback<DerivationTaskResponse>) {
|
||||
derive(keys = derivations.keys.toList(), index = 0, session = session, callback = callback)
|
||||
}
|
||||
|
||||
private fun derive(
|
||||
keys: List<ByteArrayKey>,
|
||||
index: Int,
|
||||
session: CardSession,
|
||||
callback: CompletionCallback<DerivationTaskResponse>
|
||||
) {
|
||||
if (index == keys.count()) {
|
||||
callback(CompletionResult.Success(DerivationTaskResponse(response.toMap())))
|
||||
return
|
||||
}
|
||||
|
||||
val key = keys[index]
|
||||
val paths = derivations[key]!!
|
||||
DeriveWalletPublicKeysTask(key.bytes, paths).run(session) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
response[key] = result.data
|
||||
derive(keys = keys, index = index + 1, session = session, callback = callback)
|
||||
}
|
||||
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//package com.tangem.tap.domain.tasks
|
||||
//
|
||||
//import com.tangem.common.CompletionResult
|
||||
//import com.tangem.common.core.CardSession
|
||||
//import com.tangem.common.core.CardSessionRunnable
|
||||
//import com.tangem.common.core.CompletionCallback
|
||||
//import com.tangem.common.hdWallet.DerivationPath
|
||||
//import com.tangem.common.hdWallet.ExtendedPublicKey
|
||||
//import com.tangem.operations.CommandResponse
|
||||
//import com.tangem.operations.derivation.DeriveWalletPublicKeysTask
|
||||
//import com.tangem.tap.common.extensions.ByteArrayKey
|
||||
//
|
||||
//class ExtendedPublicKeyList(
|
||||
// items: Collection<ExtendedPublicKey>
|
||||
//): ArrayList<ExtendedPublicKey>(items), CommandResponse
|
||||
//
|
||||
//
|
||||
//class DerivationTaskResponse(
|
||||
// val entries: Map<ByteArrayKey, ExtendedPublicKeyList>
|
||||
//): CommandResponse
|
||||
//
|
||||
//class DerivationTask(
|
||||
// private val derivations: Map<ByteArrayKey, List<DerivationPath>>
|
||||
//) : CardSessionRunnable<DerivationTaskResponse> {
|
||||
//
|
||||
// val response: MutableMap<ByteArrayKey, ExtendedPublicKeyList> = mutableMapOf()
|
||||
//
|
||||
// override fun run(session: CardSession, callback: CompletionCallback<DerivationTaskResponse>) {
|
||||
// derive(keys = derivations.keys.toList(), index = 0, session = session, callback = callback)
|
||||
// }
|
||||
//
|
||||
// private fun derive(
|
||||
// keys: List<ByteArrayKey>,
|
||||
// index: Int,
|
||||
// session: CardSession,
|
||||
// callback: CompletionCallback<DerivationTaskResponse>
|
||||
// ) {
|
||||
// if (index == keys.count()) {
|
||||
// callback(CompletionResult.Success(DerivationTaskResponse(response.toMap())))
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// val key = keys[index]
|
||||
// val paths = derivations[key]!!
|
||||
// DeriveWalletPublicKeysTask(key.bytes, paths).run(session) { result ->
|
||||
// when (result) {
|
||||
// is CompletionResult.Success -> {
|
||||
// response[key] = result.data[key]
|
||||
// derive(keys = keys, index = index + 1, session = session, callback = callback)
|
||||
// }
|
||||
// is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
|
@ -6,16 +6,16 @@ import com.tangem.common.core.CardSession
|
|||
import com.tangem.common.core.CardSessionRunnable
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.common.hdWallet.ExtendedPublicKey
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.operations.CommandResponse
|
||||
import com.tangem.operations.backup.PrimaryCard
|
||||
import com.tangem.operations.backup.StartPrimaryCardLinkingTask
|
||||
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import com.tangem.operations.wallet.CreateWalletResponse
|
||||
import com.tangem.operations.wallet.CreateWalletTask
|
||||
import com.tangem.tap.common.extensions.toMapKey
|
||||
import com.tangem.tap.domain.ProductType
|
||||
import com.tangem.tap.domain.TapWorkarounds.getTangemNoteBlockchain
|
||||
import com.tangem.tap.domain.tasks.DerivationTask
|
||||
import com.tangem.tap.domain.tasks.product.CreateWalletsTask
|
||||
import com.tangem.tap.domain.tasks.product.KeyWalletPublicKey
|
||||
import com.tangem.tap.domain.tasks.product.ProductCommandProcessor
|
||||
|
|
@ -24,7 +24,7 @@ import com.tangem.tap.domain.tasks.product.getCurvesForNonCreatedWallets
|
|||
|
||||
data class CreateProductWalletTaskResponse(
|
||||
val card: Card,
|
||||
val derivedKeys: Map<KeyWalletPublicKey, List<ExtendedPublicKey>> = mapOf(),
|
||||
val derivedKeys: Map<KeyWalletPublicKey, ExtendedPublicKeysMap> = mapOf(),
|
||||
val primaryCard: PrimaryCard? = null
|
||||
) : CommandResponse
|
||||
|
||||
|
|
@ -170,7 +170,7 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
|
|||
return
|
||||
}
|
||||
|
||||
DerivationTask(mapOf(response.wallet.publicKey.toMapKey() to derivationPaths))
|
||||
DeriveMultipleWalletPublicKeysTask(mapOf(response.wallet.publicKey.toMapKey() to derivationPaths))
|
||||
.run(session) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
|
|
|
|||
|
|
@ -12,19 +12,20 @@ import com.tangem.common.core.CardSession
|
|||
import com.tangem.common.core.CardSessionRunnable
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.common.hdWallet.DerivationPath
|
||||
import com.tangem.common.hdWallet.ExtendedPublicKey
|
||||
import com.tangem.operations.CommandResponse
|
||||
import com.tangem.operations.PreflightReadMode
|
||||
import com.tangem.operations.PreflightReadTask
|
||||
import com.tangem.operations.ScanTask
|
||||
import com.tangem.operations.backup.PrimaryCard
|
||||
import com.tangem.operations.backup.StartPrimaryCardLinkingTask
|
||||
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
|
||||
import com.tangem.tap.common.extensions.ByteArrayKey
|
||||
import com.tangem.tap.common.extensions.toMapKey
|
||||
import com.tangem.tap.domain.ProductType
|
||||
import com.tangem.tap.domain.TapSdkError
|
||||
import com.tangem.tap.domain.TapWorkarounds
|
||||
|
|
@ -33,7 +34,6 @@ import com.tangem.tap.domain.TapWorkarounds.isExcluded
|
|||
import com.tangem.tap.domain.TapWorkarounds.isNotSupportedInThatRelease
|
||||
import com.tangem.tap.domain.extensions.getPrimaryCurve
|
||||
import com.tangem.tap.domain.extensions.getSingleWallet
|
||||
import com.tangem.tap.domain.tasks.DerivationTask
|
||||
import com.tangem.tap.domain.tokens.CurrenciesRepository
|
||||
import com.tangem.tap.domain.twins.TwinsHelper
|
||||
import com.tangem.tap.preferencesStorage
|
||||
|
|
@ -43,7 +43,7 @@ data class ScanResponse(
|
|||
val productType: ProductType,
|
||||
val walletData: WalletData?,
|
||||
val secondTwinPublicKey: String? = null,
|
||||
val derivedKeys: Map<KeyWalletPublicKey, List<ExtendedPublicKey>> = mapOf(),
|
||||
val derivedKeys: Map<KeyWalletPublicKey, ExtendedPublicKeysMap> = mapOf(),
|
||||
val primaryCard: PrimaryCard? = null
|
||||
) : CommandResponse {
|
||||
|
||||
|
|
@ -253,7 +253,7 @@ private class ScanWalletProcessor(
|
|||
return
|
||||
}
|
||||
|
||||
DerivationTask(derivations).run(session) { result ->
|
||||
DeriveMultipleWalletPublicKeysTask(derivations).run(session) { result ->
|
||||
when (result) {
|
||||
is CompletionResult.Success -> {
|
||||
val response = ScanResponse(
|
||||
|
|
|
|||
|
|
@ -6,9 +6,10 @@ import android.animation.PropertyValuesHolder
|
|||
import android.view.View
|
||||
import androidx.core.animation.doOnEnd
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.tap.common.leapfrogWidget.LeapView
|
||||
import com.tangem.tap.common.leapfrogWidget.LeapViewState
|
||||
import com.tangem.tap.common.leapfrogWidget.LeapfrogWidget
|
||||
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapView
|
||||
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapViewState
|
||||
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
|
||||
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import com.tangem.common.core.CardSessionRunnable
|
|||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.calculateSha256
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.common.files.FileHashHelper
|
||||
import com.tangem.operations.files.FileHashHelper
|
||||
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
|
||||
import com.tangem.operations.issuerAndUserData.ReadIssuerDataResponse
|
||||
import com.tangem.operations.issuerAndUserData.WriteIssuerDataCommand
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ import com.squareup.picasso.Picasso
|
|||
import com.tangem.Message
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
|
||||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.common.leapfrogWidget.LeapfrogWidget
|
||||
import com.tangem.tap.common.redux.navigation.AppScreen
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
import com.tangem.tap.common.redux.navigation.ShareElement
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ import android.animation.PropertyValuesHolder
|
|||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.core.animation.doOnEnd
|
||||
import com.tangem.tap.common.leapfrogWidget.LeapView
|
||||
import com.tangem.tap.common.leapfrogWidget.LeapViewState
|
||||
import com.tangem.tap.common.leapfrogWidget.LeapfrogWidget
|
||||
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapView
|
||||
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapViewState
|
||||
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
|
||||
|
||||
class BackupCardsWidget(
|
||||
val leapfrogWidget: LeapfrogWidget,
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ import com.google.android.material.tabs.TabLayoutMediator
|
|||
import com.squareup.picasso.Picasso
|
||||
import com.tangem.common.CardIdFormatter
|
||||
import com.tangem.common.core.CardIdDisplayFormat
|
||||
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
|
||||
import com.tangem.tap.common.extensions.dispatchOpenUrl
|
||||
import com.tangem.tap.common.extensions.hide
|
||||
import com.tangem.tap.common.extensions.show
|
||||
import com.tangem.tap.common.leapfrogWidget.LeapfrogWidget
|
||||
import com.tangem.tap.common.postUi
|
||||
import com.tangem.tap.features.FragmentOnBackPressedHandler
|
||||
import com.tangem.tap.features.addBackPressHandler
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import com.tangem.blockchain.common.Blockchain
|
|||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.common.hdWallet.DerivationPath
|
||||
import com.tangem.common.hdWallet.ExtendedPublicKey
|
||||
import com.tangem.tap.common.extensions.ByteArrayKey
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import com.tangem.tap.common.extensions.dispatchErrorNotification
|
||||
import com.tangem.tap.common.extensions.toMapKey
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.common.redux.navigation.NavigationAction
|
||||
|
|
@ -94,14 +94,14 @@ class TokensMiddleware {
|
|||
is CompletionResult.Success -> {
|
||||
val newDerivedKeys = result.data.entries
|
||||
val updatedDerivedKeys =
|
||||
mutableMapOf<KeyWalletPublicKey, List<ExtendedPublicKey>>()
|
||||
mutableMapOf<KeyWalletPublicKey, ExtendedPublicKeysMap>()
|
||||
|
||||
newDerivedKeys.forEach { entry ->
|
||||
val derivationData = derivationDataList.find {
|
||||
it.mapKeyOfWalletPublicKey == entry.key
|
||||
} ?: return@forEach
|
||||
updatedDerivedKeys[entry.key] =
|
||||
derivationData.alreadyDerivedKeys + entry.value.toList()
|
||||
ExtendedPublicKeysMap(derivationData.alreadyDerivedKeys + entry.value)
|
||||
}
|
||||
|
||||
val updatedScanResponse = scanResponse.copy(
|
||||
|
|
@ -133,9 +133,9 @@ class TokensMiddleware {
|
|||
.mapNotNull { it.derivationPath() }
|
||||
|
||||
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
|
||||
val alreadyDerivedKeys =
|
||||
scanResponse.derivedKeys[mapKeyOfWalletPublicKey]?.toMutableList() ?: mutableListOf()
|
||||
val alreadyDerivedPaths = alreadyDerivedKeys.map { it.derivationPath }
|
||||
val alreadyDerivedKeys: ExtendedPublicKeysMap =
|
||||
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()
|
||||
|
||||
val toDerive = derivationPathsCandidates.filterNot { alreadyDerivedPaths.contains(it) }
|
||||
return DerivationData(
|
||||
|
|
@ -147,7 +147,7 @@ class TokensMiddleware {
|
|||
|
||||
private class DerivationData(
|
||||
val derivations: Pair<ByteArrayKey, List<DerivationPath>>,
|
||||
val alreadyDerivedKeys: List<ExtendedPublicKey>,
|
||||
val alreadyDerivedKeys: ExtendedPublicKeysMap,
|
||||
val mapKeyOfWalletPublicKey: ByteArrayKey
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue