142 lines
5.2 KiB
Swift
142 lines
5.2 KiB
Swift
import SwiftUI
|
||
|
||
// MARK: - Карточка-герой
|
||
|
||
/// Одна пара крупно: заголовок, сквозной график, цена и изменение за 24ч.
|
||
struct HeroCard: View {
|
||
let quote: Quote
|
||
let updatedAt: Date
|
||
/// Горизонтальный отступ контейнера — график компенсирует его
|
||
/// и уходит под края виджета, как в макете.
|
||
let horizontalPadding: CGFloat
|
||
let iconSize: CGFloat
|
||
let titleSize: CGFloat
|
||
let priceSize: CGFloat
|
||
let chartHeight: CGFloat
|
||
var showFullDate = true
|
||
var showAbsolute = true
|
||
|
||
private var thumb: SymbolThumb { quote.thumb }
|
||
|
||
var body: some View {
|
||
VStack(alignment: .leading, spacing: 0) {
|
||
PairHeader(thumb: thumb, icon: quote.icon, iconSize: iconSize, titleSize: titleSize)
|
||
|
||
UpdatedAt(date: updatedAt, fontSize: titleSize * 0.62, withDate: showFullDate)
|
||
.padding(.top, titleSize * 0.45)
|
||
|
||
Spacer(minLength: 6)
|
||
|
||
chart
|
||
.frame(height: chartHeight)
|
||
.padding(.horizontal, -horizontalPadding)
|
||
|
||
Spacer(minLength: 6)
|
||
|
||
PriceLabel(thumb: thumb, size: priceSize)
|
||
|
||
StatsLine(thumb: thumb, fontSize: titleSize * 0.78, showAbsolute: showAbsolute)
|
||
.padding(.top, titleSize * 0.4)
|
||
}
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var chart: some View {
|
||
if quote.series.count > 1 {
|
||
Sparkline(
|
||
values: quote.series,
|
||
positive: thumb.isPositive,
|
||
lineWidth: 2,
|
||
pulseSize: chartHeight * 0.26
|
||
)
|
||
} else {
|
||
// Нет истории — держим ритм макета ровной линией.
|
||
Rectangle()
|
||
.fill(
|
||
LinearGradient(
|
||
colors: [
|
||
RapiraPalette.lineEdge(thumb.isPositive),
|
||
RapiraPalette.line(thumb.isPositive).opacity(0.5),
|
||
RapiraPalette.lineEdge(thumb.isPositive),
|
||
],
|
||
startPoint: .leading, endPoint: .trailing
|
||
)
|
||
)
|
||
.frame(height: 1.5)
|
||
.frame(maxHeight: .infinity, alignment: .center)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Строка списка
|
||
|
||
/// Компактная строка: иконка, пара, мини-график, цена и процент.
|
||
struct CryptoRow: View {
|
||
let quote: Quote
|
||
let compact: Bool
|
||
|
||
private var thumb: SymbolThumb { quote.thumb }
|
||
private var iconSize: CGFloat { compact ? 21 : 23 }
|
||
|
||
/// График занимает почти весь зазор между названием и ценой: на узком
|
||
/// поле линия выглядит сплюснутой, поэтому тянем её вширь.
|
||
/// В большом виджете строк больше — там график ниже.
|
||
private var chartSize: CGSize {
|
||
compact ? CGSize(width: 92, height: 28) : CGSize(width: 88, height: 26)
|
||
}
|
||
|
||
var body: some View {
|
||
HStack(spacing: 8) {
|
||
CoinIconView(currency: thumb.quoteCurrency, data: quote.icon, size: iconSize)
|
||
|
||
VStack(alignment: .leading, spacing: 0) {
|
||
HStack(spacing: 1) {
|
||
Text(thumb.quoteCurrency)
|
||
.foregroundStyle(.white)
|
||
Text("/\(thumb.baseCurrency)")
|
||
.foregroundStyle(.white.opacity(0.45))
|
||
}
|
||
.font(.system(size: compact ? 12 : 13, weight: .bold))
|
||
.lineLimit(1)
|
||
// Длинные тикеры вроде MATIC/USDT сжимаются, а не обрезаются.
|
||
.minimumScaleFactor(0.8)
|
||
|
||
Text(thumb.quoteCurrencyName)
|
||
.font(.system(size: compact ? 9 : 10, weight: .medium))
|
||
.foregroundStyle(.white.opacity(0.35))
|
||
.lineLimit(1)
|
||
}
|
||
|
||
Spacer(minLength: 4)
|
||
|
||
if quote.series.count > 1 {
|
||
Sparkline(
|
||
values: quote.series,
|
||
positive: thumb.isPositive,
|
||
lineWidth: 1.5,
|
||
pulseSize: 7,
|
||
areaOpacity: 0.16,
|
||
trailingRoom: 0.6,
|
||
isFullBleed: false
|
||
)
|
||
.frame(width: chartSize.width, height: chartSize.height)
|
||
}
|
||
|
||
VStack(alignment: .trailing, spacing: 2) {
|
||
Text(thumb.priceFormatted)
|
||
.font(.system(size: compact ? 12 : 13, weight: .bold))
|
||
.monospacedDigit()
|
||
.foregroundStyle(.white)
|
||
.lineLimit(1)
|
||
.minimumScaleFactor(0.6)
|
||
|
||
Text(thumb.changePercent)
|
||
.font(.system(size: compact ? 10 : 10.5, weight: .semibold))
|
||
.monospacedDigit()
|
||
.foregroundStyle(RapiraPalette.stat(thumb.isPositive))
|
||
}
|
||
.frame(minWidth: compact ? 62 : 70, alignment: .trailing)
|
||
}
|
||
}
|
||
}
|