Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 33 additions & 11 deletions Usage4AI/Usage4AIApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,41 @@ struct MenuBarLabel: View {
min(1.0, Double(usagePercentage) / 100.0)
}

var body: some View {
HStack(spacing: 5) {
MenuBarProgressBars(
usageProgress: usageProgress,
timeProgress: manager.timeProgress,
statusColor: manager.statusColor
)

Text("\(usagePercentage)%")
.font(.system(size: 10, weight: .medium, design: .monospaced))
.frame(width: 28, alignment: .trailing)
}
}
}

/// Separate view for progress bars with equatable conformance to prevent unnecessary redraws
struct MenuBarProgressBars: View, Equatable {
let usageProgress: Double
let timeProgress: Double
let statusColor: Color

static func == (lhs: MenuBarProgressBars, rhs: MenuBarProgressBars) -> Bool {
// Only redraw when values actually change (with small tolerance for floating point)
abs(lhs.usageProgress - rhs.usageProgress) < 0.001 &&
abs(lhs.timeProgress - rhs.timeProgress) < 0.001 &&
lhs.statusColor == rhs.statusColor
}

private var progressBarsImage: NSImage {
let barWidth = Constants.MenuBar.barWidth
let barHeight = Constants.MenuBar.barHeight
let spacing = Constants.MenuBar.barSpacing
let totalHeight = barHeight * 2 + spacing

let image = NSImage(size: NSSize(width: barWidth, height: totalHeight), flipped: true) { rect in
let image = NSImage(size: NSSize(width: barWidth, height: totalHeight), flipped: true) { _ in
let cornerRadius = Constants.MenuBar.cornerRadius

// Top bar background
Expand All @@ -78,7 +106,7 @@ struct MenuBarLabel: View {
let progressWidth = max(barWidth * self.usageProgress, cornerRadius * 2)
let topProgressRect = NSRect(x: 0, y: 0, width: progressWidth, height: barHeight)
let topProgressPath = NSBezierPath(roundedRect: topProgressRect, xRadius: cornerRadius, yRadius: cornerRadius)
NSColor(self.manager.statusColor).setFill()
NSColor(self.statusColor).setFill()
topProgressPath.fill()
}

Expand All @@ -90,8 +118,8 @@ struct MenuBarLabel: View {
bottomBgPath.fill()

// Bottom bar progress (time)
if self.manager.timeProgress > 0 {
let timeProgressWidth = max(barWidth * self.manager.timeProgress, cornerRadius * 2)
if self.timeProgress > 0 {
let timeProgressWidth = max(barWidth * self.timeProgress, cornerRadius * 2)
let bottomProgressRect = NSRect(x: 0, y: bottomY, width: timeProgressWidth, height: barHeight)
let bottomProgressPath = NSBezierPath(roundedRect: bottomProgressRect, xRadius: cornerRadius, yRadius: cornerRadius)
NSColor.cyan.setFill()
Expand All @@ -106,13 +134,7 @@ struct MenuBarLabel: View {
}

var body: some View {
HStack(spacing: 5) {
Image(nsImage: progressBarsImage)

Text("\(usagePercentage)%")
.font(.system(size: 10, weight: .medium, design: .monospaced))
.frame(width: 28, alignment: .trailing)
}
Image(nsImage: progressBarsImage)
}
}

Expand Down
43 changes: 32 additions & 11 deletions Usage4AI/UsageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,21 +94,11 @@ struct UsageView: View {

Spacer()

Button(action: {
RefreshButton(isLoading: manager.isLoading) {
Task {
await manager.fetchUsage()
}
}) {
Image(systemName: "arrow.clockwise")
.font(.caption)
.rotationEffect(.degrees(manager.isLoading ? 360 : 0))
.animation(
manager.isLoading ? .linear(duration: 1).repeatForever(autoreverses: false) : .default,
value: manager.isLoading
)
}
.buttonStyle(.borderless)
.disabled(manager.isLoading)
}

Divider()
Expand Down Expand Up @@ -268,6 +258,37 @@ struct UsageLimitRow: View {
}
}

/// Isolated refresh button to prevent animation from triggering parent view redraws
struct RefreshButton: View {
let isLoading: Bool
let action: () -> Void

@State private var rotation: Double = 0

var body: some View {
Button(action: action) {
Image(systemName: "arrow.clockwise")
.font(.caption)
.rotationEffect(.degrees(rotation))
}
.buttonStyle(.borderless)
.disabled(isLoading)
.onChange(of: isLoading) { _, newValue in
if newValue {
// Start continuous rotation
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
rotation = 360
}
} else {
// Stop animation and reset
withAnimation(.default) {
rotation = 0
}
}
}
}
}

// Preview requires Xcode
// #Preview {
// UsageView(manager: UsageManager())
Expand Down