319cdc8b18
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
129 lines
4.6 KiB
Swift
129 lines
4.6 KiB
Swift
import SwiftUI
|
|
|
|
// MARK: - Claude Plan Card View
|
|
|
|
struct ClaudePlanCardView: View {
|
|
let usage: PlanUsage? // nil = installed, no data yet
|
|
|
|
@State private var now = Date()
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
// Header
|
|
HStack(spacing: 6) {
|
|
let dominant = usage.flatMap { ClaudePlanGauge.dominantPct($0) }
|
|
Circle()
|
|
.fill(Color(hex: ClaudePlanGauge.color(for: dominant)))
|
|
.frame(width: 7, height: 7)
|
|
Text("План Claude")
|
|
.font(.system(size: 12, weight: .semibold))
|
|
.foregroundColor(Color(hex: "#F5F6F8"))
|
|
Text(subtitleText)
|
|
.font(.system(size: 11))
|
|
.foregroundColor(Color(hex: "#8E939C"))
|
|
.lineLimit(1)
|
|
.fixedSize()
|
|
}
|
|
.padding(.top, 6)
|
|
.padding(.leading, 108)
|
|
.padding(.trailing, 36)
|
|
|
|
// Gauge rows
|
|
VStack(alignment: .leading, spacing: 5) {
|
|
GaugeRowView(label: "5 часов", window: usage?.fiveHour, now: now)
|
|
GaugeRowView(label: "Неделя", window: usage?.sevenDay, now: now, weekly: true)
|
|
}
|
|
.padding(.top, 8)
|
|
.padding(.leading, 108)
|
|
.padding(.trailing, 12)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .topLeading)
|
|
.padding(.top, 4)
|
|
// Update countdown every 30s, only while visible
|
|
.background(
|
|
TimelineView(.periodic(from: .now, by: 30)) { ctx in
|
|
Color.clear.onChange(of: ctx.date) { _, d in now = d }
|
|
}
|
|
)
|
|
}
|
|
|
|
private var subtitleText: String {
|
|
guard let usage else { return "Жду ответа Claude Code" }
|
|
let diff = now.timeIntervalSince(usage.updatedAt)
|
|
if diff < 60 { return "только что" }
|
|
let mins = Int(diff / 60)
|
|
if mins < 60 { return "\(mins) мин назад" }
|
|
return "\(mins / 60) ч назад"
|
|
}
|
|
}
|
|
|
|
// MARK: - Gauge Row
|
|
|
|
// DateFormatter created once, in Russian, for the weekly reset label
|
|
private let weeklyResetFormatter: DateFormatter = {
|
|
let fmt = DateFormatter()
|
|
fmt.locale = Locale(identifier: "ru_RU")
|
|
fmt.dateFormat = "EEE H:mm"
|
|
return fmt
|
|
}()
|
|
|
|
private struct GaugeRowView: View {
|
|
let label: String
|
|
let window: PlanWindow?
|
|
let now: Date
|
|
var weekly: Bool = false
|
|
|
|
var body: some View {
|
|
HStack(spacing: 5) {
|
|
Text(label)
|
|
.font(.system(size: 11))
|
|
.foregroundColor(Color(hex: "#6B7079"))
|
|
.frame(width: 46, alignment: .leading)
|
|
if let w = window {
|
|
let pct = ClaudePlanGauge.effectivePct(w)
|
|
let accent = Color(hex: ClaudePlanGauge.color(for: pct))
|
|
// Fixed-width bar (~50pt)
|
|
ZStack(alignment: .leading) {
|
|
Capsule()
|
|
.fill(Color.white.opacity(0.08))
|
|
.frame(width: 50, height: 4)
|
|
Capsule()
|
|
.fill(accent)
|
|
.frame(width: max(0, 50 * CGFloat(pct / 100)), height: 4)
|
|
}
|
|
.frame(width: 50, height: 4)
|
|
Text("\(Int(pct.rounded()))%")
|
|
.font(.system(size: 11, weight: .semibold))
|
|
.foregroundColor(Color(hex: "#C5C8CD"))
|
|
.monospacedDigit()
|
|
.frame(width: 30, alignment: .trailing)
|
|
Image(systemName: "arrow.clockwise")
|
|
.font(.system(size: 8))
|
|
.foregroundColor(Color(hex: "#6B7079"))
|
|
Text(resetLabel(w))
|
|
.font(.system(size: 10))
|
|
.foregroundColor(Color(hex: "#6B7079"))
|
|
.fixedSize()
|
|
} else {
|
|
Text("—")
|
|
.font(.system(size: 11))
|
|
.foregroundColor(Color(hex: "#6B7079"))
|
|
Spacer()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func resetLabel(_ w: PlanWindow) -> String {
|
|
let secs = w.resetsAt.timeIntervalSince(now)
|
|
guard secs > 0 else { return "Сброс…" }
|
|
if weekly {
|
|
return weeklyResetFormatter.string(from: w.resetsAt)
|
|
} else {
|
|
let h = Int(secs / 3600)
|
|
let m = Int((secs.truncatingRemainder(dividingBy: 3600)) / 60)
|
|
if h > 0 { return "через \(h) ч \(m) мин" }
|
|
return "через \(m) мин"
|
|
}
|
|
}
|
|
}
|