148 lines
4.3 KiB
Swift
148 lines
4.3 KiB
Swift
import WidgetKit
|
||
import SwiftUI
|
||
|
||
// MARK: - Маленький
|
||
|
||
/// Герой-карточка первой пары в сжатой компоновке.
|
||
struct SmallWidgetView: View {
|
||
let entry: CryptoEntry
|
||
|
||
private let padding: CGFloat = 14
|
||
|
||
var body: some View {
|
||
Group {
|
||
if let hero = entry.quotes.first {
|
||
HeroCard(
|
||
quote: hero,
|
||
updatedAt: entry.date,
|
||
horizontalPadding: padding,
|
||
iconSize: 20,
|
||
titleSize: 13,
|
||
priceSize: 21,
|
||
chartHeight: 40,
|
||
showFullDate: false,
|
||
showAbsolute: false
|
||
)
|
||
.padding(padding)
|
||
} else {
|
||
EmptyState(reason: entry.emptyReason)
|
||
}
|
||
}
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
|
||
.containerBackground(for: .widget) { RapiraBackground() }
|
||
}
|
||
}
|
||
|
||
// MARK: - Средний
|
||
|
||
/// Список до трёх пар со временем обновления в шапке.
|
||
struct MediumWidgetView: View {
|
||
let entry: CryptoEntry
|
||
|
||
var body: some View {
|
||
Group {
|
||
if entry.quotes.isEmpty {
|
||
EmptyState(reason: entry.emptyReason)
|
||
} else {
|
||
list
|
||
}
|
||
}
|
||
.containerBackground(for: .widget) { RapiraBackground() }
|
||
}
|
||
|
||
private var list: some View {
|
||
VStack(spacing: 0) {
|
||
HStack {
|
||
UpdatedAt(date: entry.date, fontSize: 9.5)
|
||
Spacer()
|
||
RapiraMark(size: 13)
|
||
}
|
||
.padding(.bottom, 2)
|
||
|
||
let quotes = Array(entry.quotes.prefix(3))
|
||
ForEach(Array(quotes.enumerated()), id: \.element.id) { index, quote in
|
||
CryptoRow(quote: quote, compact: true)
|
||
.frame(maxHeight: .infinity)
|
||
if index < quotes.count - 1 {
|
||
RapiraPalette.divider.frame(height: 1).padding(.leading, 32)
|
||
}
|
||
}
|
||
}
|
||
.padding(.horizontal, 14)
|
||
.padding(.vertical, 8)
|
||
}
|
||
}
|
||
|
||
// MARK: - Большой
|
||
|
||
/// Герой первой пары + до четырёх остальных списком.
|
||
struct LargeWidgetView: View {
|
||
let entry: CryptoEntry
|
||
|
||
private let padding: CGFloat = 16
|
||
|
||
var body: some View {
|
||
Group {
|
||
if let hero = entry.quotes.first {
|
||
content(hero: hero, rest: Array(entry.quotes.dropFirst().prefix(4)))
|
||
} else {
|
||
EmptyState(reason: entry.emptyReason)
|
||
}
|
||
}
|
||
.containerBackground(for: .widget) { RapiraBackground() }
|
||
}
|
||
|
||
private func content(hero: Quote, rest: [Quote]) -> some View {
|
||
VStack(alignment: .leading, spacing: 0) {
|
||
HeroCard(
|
||
quote: hero,
|
||
updatedAt: entry.date,
|
||
horizontalPadding: padding,
|
||
iconSize: 28,
|
||
titleSize: 19,
|
||
priceSize: 32,
|
||
chartHeight: 70
|
||
)
|
||
|
||
if !rest.isEmpty {
|
||
RapiraPalette.divider
|
||
.frame(height: 1)
|
||
.padding(.top, 14)
|
||
.padding(.horizontal, -padding)
|
||
|
||
ForEach(Array(rest.enumerated()), id: \.element.id) { index, quote in
|
||
CryptoRow(quote: quote, compact: false)
|
||
.frame(maxHeight: .infinity)
|
||
if index < rest.count - 1 {
|
||
RapiraPalette.divider.frame(height: 1).padding(.leading, 34)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.padding(padding)
|
||
}
|
||
}
|
||
|
||
// MARK: - Диспетчер размеров
|
||
|
||
struct CryptoWidgetEntryView: View {
|
||
let entry: CryptoEntry
|
||
|
||
@Environment(\.widgetFamily) private var family
|
||
|
||
var body: some View {
|
||
switch family {
|
||
case .systemSmall: SmallWidgetView(entry: entry)
|
||
case .systemMedium: MediumWidgetView(entry: entry)
|
||
case .systemLarge: LargeWidgetView(entry: entry)
|
||
default: MediumWidgetView(entry: entry)
|
||
}
|
||
}
|
||
}
|
||
|
||
extension CryptoEntry {
|
||
var emptyReason: EmptyState.Reason {
|
||
dataUnavailable ? .noData : .noSelection
|
||
}
|
||
}
|