6de2455917
- Added GLOBAL_MARKETS_TITLE to all translation files - Updated footer with 12 markets (4 active + 8 upcoming) - Translated market section to: zh-cn, zh-tw, ja, ko, th, vi, id, ms, hi - Built and deployed to production - CloudFront invalidation: I3RTMXVFDJXWLG3SYX208OP1CC
129 lines
4.5 KiB
Swift
129 lines
4.5 KiB
Swift
import SwiftUI
|
||
|
||
struct HealthRing: View {
|
||
let progress: Double // 0.0 – 1.0
|
||
let color: Color
|
||
var lineWidth: CGFloat = 12
|
||
var size: CGFloat = 120
|
||
var label: String? = nil
|
||
|
||
@State private var animatedProgress: Double = 0
|
||
|
||
private var clampedProgress: Double { min(max(progress, 0), 1) }
|
||
private var percentText: String { "\(Int((animatedProgress * 100).rounded()))" }
|
||
|
||
var body: some View {
|
||
ZStack {
|
||
Circle()
|
||
.stroke(color.opacity(0.15), lineWidth: lineWidth)
|
||
|
||
Circle()
|
||
.trim(from: 0, to: animatedProgress)
|
||
.stroke(
|
||
color,
|
||
style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)
|
||
)
|
||
.rotationEffect(.degrees(-90))
|
||
.animation(
|
||
.spring(response: 0.85, dampingFraction: 0.72),
|
||
value: animatedProgress
|
||
)
|
||
|
||
VStack(spacing: 2) {
|
||
HStack(alignment: .firstTextBaseline, spacing: 1) {
|
||
Text(percentText)
|
||
.font(.system(size: size * 0.22, weight: .bold, design: .rounded))
|
||
.foregroundStyle(.primary)
|
||
.contentTransition(.numericText(countsDown: animatedProgress < clampedProgress))
|
||
.animation(
|
||
.spring(response: 0.85, dampingFraction: 0.72),
|
||
value: animatedProgress
|
||
)
|
||
Text("%")
|
||
.font(.system(size: size * 0.13, weight: .semibold, design: .rounded))
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
if let label {
|
||
Text(label)
|
||
.font(.system(size: size * 0.1, weight: .medium))
|
||
.foregroundStyle(.secondary)
|
||
.lineLimit(1)
|
||
.minimumScaleFactor(0.5)
|
||
.frame(maxWidth: size * 0.55)
|
||
}
|
||
}
|
||
}
|
||
.frame(width: size, height: size)
|
||
.onAppear {
|
||
animatedProgress = clampedProgress
|
||
}
|
||
.onChange(of: progress) { _, newValue in
|
||
animatedProgress = min(max(newValue, 0), 1)
|
||
}
|
||
}
|
||
}
|
||
|
||
#Preview {
|
||
@Previewable @State var cpu: Double = 0.72
|
||
@Previewable @State var mem: Double = 0.45
|
||
@Previewable @State var disk: Double = 0.88
|
||
|
||
NavigationStack {
|
||
ScrollView {
|
||
LazyVStack(spacing: 32) {
|
||
HStack(spacing: 24) {
|
||
VStack(spacing: 8) {
|
||
HealthRing(
|
||
progress: cpu,
|
||
color: Color(.systemBlue),
|
||
label: "CPU"
|
||
)
|
||
Text("CPU")
|
||
.font(.caption.weight(.medium))
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
VStack(spacing: 8) {
|
||
HealthRing(
|
||
progress: mem,
|
||
color: Color(.systemGreen),
|
||
label: "RAM"
|
||
)
|
||
Text("Memory")
|
||
.font(.caption.weight(.medium))
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
VStack(spacing: 8) {
|
||
HealthRing(
|
||
progress: disk,
|
||
color: Color(.systemOrange),
|
||
label: "Disk"
|
||
)
|
||
Text("Disk")
|
||
.font(.caption.weight(.medium))
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
}
|
||
|
||
HealthRing(
|
||
progress: 0.23,
|
||
color: Color(.systemRed),
|
||
lineWidth: 18,
|
||
size: 180,
|
||
label: "Error rate"
|
||
)
|
||
|
||
VStack(spacing: 8) {
|
||
Slider(value: $cpu, in: 0...1)
|
||
Text("CPU: \(Int(cpu * 100))%")
|
||
.font(.caption)
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
.padding(.horizontal)
|
||
}
|
||
.padding()
|
||
}
|
||
.navigationTitle("Health")
|
||
.background(Color(.systemBackground))
|
||
}
|
||
}
|