Files
brov-macbook/NotchBuddy/Sources/App/ClaudePlanCardView.swift
T

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) мин"
}
}
}