Updated on 2026-08-14
This commit is contained in:
parent
ca85fd3a6d
commit
956bc7cd75
6 changed files with 40 additions and 45 deletions
|
|
@ -98,6 +98,7 @@
|
|||
<string name="address_book_add_address">Add address</string>
|
||||
<string name="address_book_add_address_description">Add address and select network</string>
|
||||
<string name="address_book_add_contact">Add contact</string>
|
||||
<string name="address_book_address_copied">Address copied</string>
|
||||
<string name="address_book_address_taken_error">This address is already saved as %1$s</string>
|
||||
<plurals name="address_book_addresses">
|
||||
<item quantity="one">%d address</item>
|
||||
|
|
@ -119,7 +120,6 @@
|
|||
<string name="address_book_keep_editing">Keep editing</string>
|
||||
<string name="address_book_max_networks_alert_description">You can not create more than 20 addresses. Delete one to add new.</string>
|
||||
<string name="address_book_max_networks_alert_title">Can\'t add new address</string>
|
||||
<string name="address_book_memo_footer">Memo / Destination Tag is a code separating transactions to a shared recipient in a crypto network.\n**Caution: Missing a memo may lead to fund loss.**</string>
|
||||
<string name="address_book_name_empty_error">Contact name is required</string>
|
||||
<string name="address_book_name_invalid_chars_error">Contact name contains invalid characters</string>
|
||||
<string name="address_book_name_max_chars_error">Contact name must not exceed 50 characters</string>
|
||||
|
|
@ -887,13 +887,14 @@
|
|||
<string name="manage_tokens_unavailable_description">The selected token is currently unavailable for actions within the crypto wallet. But worry not, you can express your interest by upvoting it.</string>
|
||||
<string name="manage_tokens_unavailable_vote">Upvote</string>
|
||||
<string name="manage_tokens_wallet_support_only_one_network_title">The wallet doesn\'t support more than one network</string>
|
||||
<string name="market_chart_ai_total">Al Total:</string>
|
||||
<string name="market_chart_ai_total">AI Total:</string>
|
||||
<string name="market_chart_ask_for_ai_summary_button">Ask for AI summary</string>
|
||||
<plurals name="market_chart_assets_android">
|
||||
<item quantity="one">%d asset</item>
|
||||
<item quantity="other">%d assets</item>
|
||||
</plurals>
|
||||
<string name="market_chart_buble_no_data">No data</string>
|
||||
<string name="market_chart_buble_total_value">Total value</string>
|
||||
<string name="market_chart_bubble_no_data">No data</string>
|
||||
<string name="market_chart_bubble_total_value">Total value</string>
|
||||
<string name="market_chart_can_not_load_data">Can’t load data</string>
|
||||
<string name="market_chart_top_holding">Top holding %s%%</string>
|
||||
<string name="markets_about_coin_header">About coin</string>
|
||||
|
|
|
|||
|
|
@ -35,15 +35,17 @@ import androidx.compose.ui.graphics.toArgb
|
|||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.toSize
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.toSize
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.features.foryou.impl.components.state.DonutSegment
|
||||
import kotlin.math.min
|
||||
import com.tangem.features.foryou.impl.R
|
||||
import com.tangem.features.foryou.impl.components.state.DonutSegment
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.hypot
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Ring (donut) chart drawn behind a center [content] slot.
|
||||
|
|
@ -122,7 +124,6 @@ internal fun DonutChart(
|
|||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(TangemTheme.colors3.bg.secondary)
|
||||
.then(clickModifier)
|
||||
.drawBehind {
|
||||
val arc = arcRect(strokePx)
|
||||
|
|
@ -240,11 +241,11 @@ private fun segmentIndexAt(
|
|||
val outer = min(size.width, size.height) / 2f
|
||||
val inner = outer - strokePx
|
||||
val tolerance = strokePx * 0.4f
|
||||
val dist = kotlin.math.hypot(dx, dy)
|
||||
val dist = hypot(dx, dy)
|
||||
if (dist < inner - tolerance || dist > outer + tolerance) return null
|
||||
|
||||
// Degrees clockwise from 3 o'clock — same convention as Canvas.drawArc.
|
||||
val angle = Math.toDegrees(kotlin.math.atan2(dy, dx).toDouble()).toFloat().mod(360f)
|
||||
val angle = Math.toDegrees(atan2(dy, dx).toDouble()).toFloat().mod(360f)
|
||||
|
||||
val sweeps = segments.map { it.weight.coerceIn(0f, 1f) * 360f }
|
||||
val starts = sweeps.runningFold(startAngle) { acc, sweep -> acc + sweep }
|
||||
|
|
@ -363,7 +364,7 @@ private fun PreviewDonutChart() {
|
|||
style = TangemTheme.typography3.heading.medium,
|
||||
)
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.market_chart_buble_total_value),
|
||||
text = stringResourceSafe(R.string.market_chart_bubble_total_value),
|
||||
color = TangemTheme.colors3.text.secondary,
|
||||
style = TangemTheme.typography3.body.medium,
|
||||
)
|
||||
|
|
@ -389,7 +390,7 @@ private fun PreviewDonutChartEmpty() {
|
|||
segments = emptyList(),
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.market_chart_buble_no_data),
|
||||
text = stringResourceSafe(R.string.market_chart_bubble_no_data),
|
||||
color = TangemTheme.colors3.text.primary,
|
||||
style = TangemTheme.typography3.heading.medium,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ private fun PreviewDonutSegmentTooltip() {
|
|||
expandedStates = remember { MutableTransitionState(true) },
|
||||
title = "Ethereum",
|
||||
fiatValue = "$5,720.22",
|
||||
percent = "57.5%",
|
||||
percent = "57.5",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,21 +39,12 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
|
||||
/**
|
||||
* Canvas-based take on the glow divider: the line background and every blurred color blob are painted by
|
||||
* hand inside a single `Box(Modifier.drawBehind { … })`.
|
||||
* One blurred color blob painted inside [CanvasGradientDivider].
|
||||
*
|
||||
* How it differs from [GlowDotsDivider] (which layers child `Box`es with [androidx.compose.ui.draw.blur]):
|
||||
* here there are no child composables at all. Inside [androidx.compose.ui.draw.drawBehind] we clip to the
|
||||
* capsule path, fill the base color, then draw each [GlowDot] as a circle whose native [Paint] carries a
|
||||
* [BlurMaskFilter] — the canvas equivalent of a Gaussian layer blur. The clip means a blob drawn past the
|
||||
* line's bounds only paints its color onto the visible capsule.
|
||||
*
|
||||
* @param modifier Modifier for positioning. Lays out to [lineWidth]×[lineHeight].
|
||||
* @param lineWidth Width of the capsule.
|
||||
* @param lineHeight Height of the capsule.
|
||||
* @param lineColor Solid base fill of the line.
|
||||
* @param dots Color blobs; [GlowDot.offset] is the blob center relative to the line's top-center,
|
||||
* [GlowDot.size] its diameter, [GlowDot.blur] the mask-blur radius.
|
||||
* @param color Blob color.
|
||||
* @param offset Blob center, relative to the line's top-center.
|
||||
* @param height Blob height (its vertical diameter); the width is fixed by [DotWidth].
|
||||
* @param blur Mask-blur radius applied via [BlurMaskFilter].
|
||||
*/
|
||||
internal data class CanvasGlowDot(
|
||||
val color: Color,
|
||||
|
|
@ -62,6 +53,17 @@ internal data class CanvasGlowDot(
|
|||
val blur: Dp = 8.dp,
|
||||
)
|
||||
|
||||
/**
|
||||
* Canvas-based glow divider: the line background and every blurred color blob are painted by hand inside a
|
||||
* single `Box(Modifier.drawBehind { … })`, with no child composables. Inside [Modifier.drawBehind] we clip
|
||||
* to the capsule path, fill the base [LineColor], then draw each [CanvasGlowDot] as an oval whose native
|
||||
* [Paint] carries a [BlurMaskFilter] — the canvas equivalent of a Gaussian layer blur. The clip means a blob
|
||||
* drawn past the line's bounds only paints its color onto the visible capsule.
|
||||
*
|
||||
* @param modifier Modifier for positioning; the capsule height comes from the parent, so the glow scales
|
||||
* with the divider's length.
|
||||
* @param lineWidth Width of the capsule.
|
||||
*/
|
||||
@Suppress("MagicNumber", "LongParameterList", "LongMethod")
|
||||
@Composable
|
||||
internal fun CanvasGradientDivider(modifier: Modifier = Modifier, lineWidth: Dp = 2.dp) {
|
||||
|
|
@ -163,10 +165,6 @@ private data class GlowDotColors(
|
|||
val orange: Color,
|
||||
)
|
||||
|
||||
/**
|
||||
* Placeholder snake-scatter of the four Figma "selection colors", sized proportionally to [lineHeight]
|
||||
* so the glow scales with the divider's length. Tune to match Figma.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
private fun canvasDefaultDots(lineHeight: Dp, colors: GlowDotColors): List<CanvasGlowDot> {
|
||||
val step = lineHeight / 5
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ import androidx.compose.ui.input.pointer.pointerInput
|
|||
import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
|
|
@ -53,19 +52,18 @@ import com.tangem.core.ui.ds.button.SecondaryTangemButton
|
|||
import com.tangem.core.ui.ds.button.TangemButtonSize
|
||||
import com.tangem.core.ui.ds2.surface.TangemSurface
|
||||
import com.tangem.core.ui.extensions.pluralStringResourceSafe
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.LocalHazeState
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.features.foryou.impl.R
|
||||
import com.tangem.features.foryou.impl.components.state.AiInsightState
|
||||
import com.tangem.features.foryou.impl.components.state.DonutChartState
|
||||
import com.tangem.features.foryou.impl.components.state.DonutSegment
|
||||
import com.tangem.features.foryou.impl.components.state.MarketChartState
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.Int
|
||||
import com.tangem.features.foryou.impl.R
|
||||
|
||||
@Composable
|
||||
internal fun MarketChart(marketChartState: MarketChartState, modifier: Modifier = Modifier) {
|
||||
|
|
@ -149,7 +147,7 @@ private fun ColumnScope.DonutChartBlock(donutChartState: DonutChartState, cardBo
|
|||
),
|
||||
)
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.market_chart_buble_total_value),
|
||||
text = stringResourceSafe(R.string.market_chart_bubble_total_value),
|
||||
color = TangemTheme.colors3.text.secondary,
|
||||
style = TangemTheme.typography3.caption.medium,
|
||||
maxLines = 1,
|
||||
|
|
@ -159,7 +157,7 @@ private fun ColumnScope.DonutChartBlock(donutChartState: DonutChartState, cardBo
|
|||
)
|
||||
} else {
|
||||
Text(
|
||||
text = stringResourceSafe(R.string.market_chart_buble_no_data),
|
||||
text = stringResourceSafe(R.string.market_chart_bubble_no_data),
|
||||
color = TangemTheme.colors3.text.secondary,
|
||||
style = TangemTheme.typography3.body.medium,
|
||||
maxLines = 1,
|
||||
|
|
@ -239,7 +237,7 @@ private fun DonutSegmentTooltipBlock(
|
|||
@Suppress("MagicNumber")
|
||||
private fun formatSegmentPercent(weight: Float): String {
|
||||
val percent = weight.coerceIn(0f, 1f) * 100
|
||||
return if (percent % 1f == 0f) "${percent.toInt()}" else "%.2f%".format(percent)
|
||||
return if (percent % 1f == 0f) "${percent.toInt()}" else "%.2f".format(percent)
|
||||
}
|
||||
|
||||
private val DonutStrokeWidth = 28.dp
|
||||
|
|
@ -288,7 +286,7 @@ private fun AiInsightContent(aiInsightState: AiInsightState) {
|
|||
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||
onClick = currentState.askAiInsightClick,
|
||||
size = TangemButtonSize.X9,
|
||||
text = stringReference("Ask for AI summary"),
|
||||
text = resourceReference(R.string.market_chart_ask_for_ai_summary_button),
|
||||
)
|
||||
}
|
||||
is AiInsightState.Displayed -> {
|
||||
|
|
@ -368,7 +366,7 @@ private fun previewMarketChartState(scenario: MarketChartPreviewScenario): Marke
|
|||
topHoldingPercent = 0.41f,
|
||||
aiInsightState = AiInsightState.Displayed(
|
||||
"Your portfolio leans on a single asset – BTC is 42% of holdings. Stablecoins add 23% " +
|
||||
"buffer. Consider trimmng concentration for a smoother ride",
|
||||
"buffer. Consider trimming concentration for a smoother ride",
|
||||
),
|
||||
donutChartState = previewLoadedDonut(),
|
||||
)
|
||||
|
|
@ -403,13 +401,13 @@ private fun previewLoadedDonut(): DonutChartState.Loaded = DonutChartState.Loade
|
|||
fiatValue = "$5,720.22",
|
||||
),
|
||||
DonutSegment(
|
||||
weight = 0.07f,
|
||||
weight = 0.077f,
|
||||
color = TangemTheme.colors3.border.accent.violet,
|
||||
title = "Solana",
|
||||
fiatValue = "$728.30",
|
||||
),
|
||||
DonutSegment(
|
||||
weight = 0.06f,
|
||||
weight = 0.0666f,
|
||||
color = TangemTheme.colors3.border.accent.red,
|
||||
title = "Polkadot",
|
||||
fiatValue = "$624.26",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
package com.tangem.features.foryou.impl.components.state
|
||||
|
||||
import kotlin.Float
|
||||
import kotlin.collections.List
|
||||
|
||||
internal sealed class MarketChartState(
|
||||
open val donutChartState: DonutChartState,
|
||||
open val aiInsightState: AiInsightState,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue