diff --git a/ClaudeMeter/Models/AppSettings.swift b/ClaudeMeter/Models/AppSettings.swift index 3522384..b18ac2d 100644 --- a/ClaudeMeter/Models/AppSettings.swift +++ b/ClaudeMeter/Models/AppSettings.swift @@ -27,6 +27,9 @@ struct AppSettings: Codable, Equatable, Sendable { /// Whether to show Sonnet usage in the popover var isSonnetUsageShown: Bool + /// Whether to show the exact reset time alongside the relative description + var isResetTimeShown: Bool + /// Menu bar icon display style var iconStyle: IconStyle @@ -40,6 +43,7 @@ struct AppSettings: Codable, Equatable, Sendable { isFirstLaunch: true, cachedOrganizationId: nil, isSonnetUsageShown: false, + isResetTimeShown: true, iconStyle: .battery, isColoredIcon: true ) @@ -51,6 +55,7 @@ struct AppSettings: Codable, Equatable, Sendable { case isFirstLaunch = "is_first_launch" case cachedOrganizationId = "cached_organization_id" case isSonnetUsageShown = "show_sonnet_usage" + case isResetTimeShown = "show_reset_time" case iconStyle = "icon_style" case isColoredIcon = "is_colored_icon" } @@ -67,6 +72,7 @@ extension AppSettings { isFirstLaunch = try container.decodeIfPresent(Bool.self, forKey: .isFirstLaunch) ?? defaults.isFirstLaunch cachedOrganizationId = try container.decodeIfPresent(UUID.self, forKey: .cachedOrganizationId) isSonnetUsageShown = try container.decodeIfPresent(Bool.self, forKey: .isSonnetUsageShown) ?? defaults.isSonnetUsageShown + isResetTimeShown = try container.decodeIfPresent(Bool.self, forKey: .isResetTimeShown) ?? defaults.isResetTimeShown iconStyle = try container.decodeIfPresent(IconStyle.self, forKey: .iconStyle) ?? defaults.iconStyle isColoredIcon = try container.decodeIfPresent(Bool.self, forKey: .isColoredIcon) ?? defaults.isColoredIcon } diff --git a/ClaudeMeter/Models/UsageLimit.swift b/ClaudeMeter/Models/UsageLimit.swift index 847dd97..cf0c255 100644 --- a/ClaudeMeter/Models/UsageLimit.swift +++ b/ClaudeMeter/Models/UsageLimit.swift @@ -79,10 +79,18 @@ extension UsageLimit { count == 1 ? singular : "\(singular)s" } - /// Exact reset time formatted in user's timezone for tooltip display + /// Exact reset time formatted in user's timezone for tooltip display (no year) var resetTimeFormatted: String { let formatter = DateFormatter() - formatter.dateStyle = .medium + formatter.timeZone = .current + formatter.setLocalizedDateFormatFromTemplate("MMMdjmm") + return formatter.string(from: resetAt) + } + + /// Exact reset time-of-day only (no date), in the user's timezone. + var resetTimeOnlyFormatted: String { + let formatter = DateFormatter() + formatter.dateStyle = .none formatter.timeStyle = .short formatter.timeZone = .current return formatter.string(from: resetAt) diff --git a/ClaudeMeter/Views/MenuBar/UsageCardView.swift b/ClaudeMeter/Views/MenuBar/UsageCardView.swift index cc425c5..29b244d 100644 --- a/ClaudeMeter/Views/MenuBar/UsageCardView.swift +++ b/ClaudeMeter/Views/MenuBar/UsageCardView.swift @@ -14,6 +14,24 @@ struct UsageCardView: View { let icon: String let windowDuration: TimeInterval? + /// Whether to append the exact reset time in parentheses. + var showsExactResetTime: Bool = true + + /// When true, the exact reset time shows the time of day only (no date). + var usesTimeOnlyResetTimestamp: Bool = false + + /// Exact reset time string, time-only or date+time depending on the card. + private var exactResetTime: String { + usesTimeOnlyResetTimestamp ? usageLimit.resetTimeOnlyFormatted : usageLimit.resetTimeFormatted + } + + /// Reset label, optionally with the exact time appended in parentheses. + private var resetLabel: String { + showsExactResetTime + ? "Resets \(usageLimit.resetDescription) (\(exactResetTime))" + : "Resets \(usageLimit.resetDescription)" + } + var body: some View { VStack(alignment: .leading, spacing: 12) { // Header with icon and title @@ -68,7 +86,7 @@ struct UsageCardView: View { HStack(spacing: 4) { Image(systemName: "clock") .font(.caption) - Text("Resets \(usageLimit.resetDescription)") + Text(resetLabel) .font(.caption) } .help(usageLimit.resetTimeFormatted) @@ -91,7 +109,7 @@ struct UsageCardView: View { .cornerRadius(12) .accessibilityElement(children: .combine) .accessibilityLabel("\(title): \(Int(usageLimit.percentage))% used, \(usageLimit.status.accessibilityDescription)") - .accessibilityValue("Resets \(usageLimit.resetDescription)") + .accessibilityValue(resetLabel) } } diff --git a/ClaudeMeter/Views/MenuBar/UsagePopoverView.swift b/ClaudeMeter/Views/MenuBar/UsagePopoverView.swift index 199bf06..45f4803 100644 --- a/ClaudeMeter/Views/MenuBar/UsagePopoverView.swift +++ b/ClaudeMeter/Views/MenuBar/UsagePopoverView.swift @@ -92,7 +92,9 @@ struct UsagePopoverView: View { title: "5-Hour Session", usageLimit: usageData.sessionUsage, icon: "gauge.with.dots.needle.67percent", - windowDuration: Constants.Pacing.sessionWindow + windowDuration: Constants.Pacing.sessionWindow, + showsExactResetTime: appModel.settings.isResetTimeShown, + usesTimeOnlyResetTimestamp: true ) // Weekly usage card @@ -100,7 +102,8 @@ struct UsagePopoverView: View { title: "Weekly Usage", usageLimit: usageData.weeklyUsage, icon: "calendar", - windowDuration: Constants.Pacing.weeklyWindow + windowDuration: Constants.Pacing.weeklyWindow, + showsExactResetTime: appModel.settings.isResetTimeShown ) // Sonnet usage card (conditional rendering) @@ -109,7 +112,8 @@ struct UsagePopoverView: View { title: "Weekly Sonnet", usageLimit: sonnetUsage, icon: "sparkles", - windowDuration: Constants.Pacing.weeklyWindow + windowDuration: Constants.Pacing.weeklyWindow, + showsExactResetTime: appModel.settings.isResetTimeShown ) } } diff --git a/ClaudeMeter/Views/Settings/SettingsView.swift b/ClaudeMeter/Views/Settings/SettingsView.swift index 4fd2588..17f8e75 100644 --- a/ClaudeMeter/Views/Settings/SettingsView.swift +++ b/ClaudeMeter/Views/Settings/SettingsView.swift @@ -62,6 +62,7 @@ struct SettingsView: View { sessionKeySection refreshIntervalSection sonnetUsageSection + resetTimeSection iconStyleSection launchAtLoginSection } @@ -232,6 +233,28 @@ struct SettingsView: View { .clipShape(RoundedRectangle(cornerRadius: 8)) } + // MARK: - Reset Time Section + + private var resetTimeSection: some View { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("Show Reset Time") + .font(.subheadline) + Text("Display the exact time each limit resets in the menu bar popover") + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() + + Toggle("", isOn: $appModel.settings.isResetTimeShown) + .labelsHidden() + } + .padding() + .background(.quaternary.opacity(0.3)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + // MARK: - Icon Style Section private var iconStyleSection: some View {