diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a054e59..ca2d445 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "me.khodak.claudeusage" minSdk = 26 targetSdk = 34 - versionCode = 13 - versionName = "1.12" + versionCode = 14 + versionName = "1.13" } signingConfigs { diff --git a/app/src/main/java/me/khodak/claudeusage/BarRenderer.kt b/app/src/main/java/me/khodak/claudeusage/BarRenderer.kt index 5ff6378..48d78a5 100644 --- a/app/src/main/java/me/khodak/claudeusage/BarRenderer.kt +++ b/app/src/main/java/me/khodak/claudeusage/BarRenderer.kt @@ -19,7 +19,7 @@ object BarRenderer { usedPct: Int, markerPct: Int?, fillColor: Int, - tierColor: Int?, + markerColor: Int?, wPx: Int = 500, hPx: Int = 14, cornerPx: Float = 7f @@ -42,20 +42,16 @@ object BarRenderer { canvas.drawRoundRect(fill, cornerPx, cornerPx, paint) } - // Pace tick — "where you should be right now". - // Wider than before and wrapped in a white halo so it stands out on any fill color. - if (markerPct != null && tierColor != null) { + // Pace marker — a single clean tick showing "where you should be right now". + // One color (no tiers); rounded ends to match the bar. + if (markerPct != null && markerColor != null) { val m = markerPct.coerceIn(0, 100) - val coreW = (wPx * 0.022f).coerceIn(9f, 14f) // tier-colored core - val halo = coreW * 0.6f // white outline on each side + val tickW = (wPx * 0.016f).coerceIn(6f, 10f) var x = wPx * m / 100f - x = x.coerceIn(coreW / 2f + halo, wPx - coreW / 2f - halo) - // white halo - paint.color = 0xFFFFFFFF.toInt() - canvas.drawRect(x - coreW / 2f - halo, 0f, x + coreW / 2f + halo, hPx.toFloat(), paint) - // tier-colored core - paint.color = tierColor - canvas.drawRect(x - coreW / 2f, 0f, x + coreW / 2f, hPx.toFloat(), paint) + x = x.coerceIn(tickW / 2f, wPx - tickW / 2f) + paint.color = markerColor + val tick = RectF(x - tickW / 2f, 0f, x + tickW / 2f, hPx.toFloat()) + canvas.drawRoundRect(tick, tickW / 2f, tickW / 2f, paint) } return bmp diff --git a/app/src/main/java/me/khodak/claudeusage/ClaudeUsageWidget.kt b/app/src/main/java/me/khodak/claudeusage/ClaudeUsageWidget.kt index e9d2630..f77ce04 100644 --- a/app/src/main/java/me/khodak/claudeusage/ClaudeUsageWidget.kt +++ b/app/src/main/java/me/khodak/claudeusage/ClaudeUsageWidget.kt @@ -104,9 +104,8 @@ class ClaudeUsageWidget : AppWidgetProvider() { when { hasUtilization -> { val pct = apiData!!.fiveHourUtilization.toInt() - val pace = PaceCalc.compute(apiData.fiveHourUtilization, apiData.utilizationResetAtEpoch, PaceCalc.SESSION_WINDOW_MS) v.setTextViewText(R.id.tv_session_value, "$pct%") - v.setImageViewBitmap(R.id.bar_session, BarRenderer.render(pct, pace?.markerPct, SESSION_FILL, pace?.tierColor)) + v.setImageViewBitmap(R.id.bar_session, BarRenderer.render(pct, null, SESSION_FILL, null)) v.setTextViewText(R.id.tv_session_label, formatReset(apiData.utilizationResetAtEpoch)) } hasApiMessages -> { @@ -135,8 +134,8 @@ class ClaudeUsageWidget : AppWidgetProvider() { val wPct = apiData!!.weeklyUtilization.toInt() val pace = PaceCalc.compute(apiData.weeklyUtilization, apiData.weeklyResetAtEpoch, PaceCalc.WEEKLY_WINDOW_MS) v.setTextViewText(R.id.tv_weekly_value, "$wPct%") - v.setImageViewBitmap(R.id.bar_weekly, BarRenderer.render(wPct, pace?.markerPct, WEEKLY_FILL, pace?.tierColor)) - v.setTextViewText(R.id.tv_weekly_label, formatReset(apiData.weeklyResetAtEpoch)) + v.setImageViewBitmap(R.id.bar_weekly, BarRenderer.render(wPct, pace?.markerPct, WEEKLY_FILL, if (pace != null) MARKER_COLOR else null)) + v.setTextViewText(R.id.tv_weekly_label, formatResetDay(apiData.weeklyResetAtEpoch)) } else { val weeklyDays = Integer.bitCount(prefs.getWeeklyMask()) v.setTextViewText(R.id.tv_weekly_value, "${weeklyDays}d") @@ -182,9 +181,8 @@ class ClaudeUsageWidget : AppWidgetProvider() { when { hasUtilization -> { val pct = apiData!!.fiveHourUtilization.toInt() - val pace = PaceCalc.compute(apiData.fiveHourUtilization, apiData.utilizationResetAtEpoch, PaceCalc.SESSION_WINDOW_MS) v.setTextViewText(R.id.tv_session_value, "$pct%") - v.setImageViewBitmap(R.id.bar_session, BarRenderer.render(pct, pace?.markerPct, SESSION_FILL, pace?.tierColor)) + v.setImageViewBitmap(R.id.bar_session, BarRenderer.render(pct, null, SESSION_FILL, null)) v.setTextViewText(R.id.tv_session_label, formatReset(apiData.utilizationResetAtEpoch)) } hasApiMessages -> { @@ -216,8 +214,8 @@ class ClaudeUsageWidget : AppWidgetProvider() { val wPct = apiData!!.weeklyUtilization.toInt() val pace = PaceCalc.compute(apiData.weeklyUtilization, apiData.weeklyResetAtEpoch, PaceCalc.WEEKLY_WINDOW_MS) v.setTextViewText(R.id.tv_weekly_value, "$wPct%") - v.setImageViewBitmap(R.id.bar_weekly, BarRenderer.render(wPct, pace?.markerPct, WEEKLY_FILL, pace?.tierColor)) - v.setTextViewText(R.id.tv_weekly_label, formatReset(apiData.weeklyResetAtEpoch)) + v.setImageViewBitmap(R.id.bar_weekly, BarRenderer.render(wPct, pace?.markerPct, WEEKLY_FILL, if (pace != null) MARKER_COLOR else null)) + v.setTextViewText(R.id.tv_weekly_label, formatResetDay(apiData.weeklyResetAtEpoch)) } else { val weeklyDays = Integer.bitCount(prefs.getWeeklyMask()) v.setTextViewText(R.id.tv_weekly_value, "$weeklyDays d") @@ -247,6 +245,7 @@ class ClaudeUsageWidget : AppWidgetProvider() { private const val SESSION_FILL = 0xFFCC785C.toInt() private const val WEEKLY_FILL = 0xFF7B8FCC.toInt() + private const val MARKER_COLOR = 0xFFFFFFFF.toInt() // single-color pace marker (weekly only) /** Tints the header burst icon and (optionally) the PEAK text by current peak state. */ private fun applyPeak(v: RemoteViews, showText: Boolean) { @@ -271,6 +270,15 @@ class ClaudeUsageWidget : AppWidgetProvider() { } } + /** Weekly reset shown with the weekday name ("Resets Friday 3:00 PM"), never "tomorrow". */ + private fun formatResetDay(epochMs: Long): String { + if (epochMs <= 0) return "" + if (epochMs <= System.currentTimeMillis()) return "Resets soon" + val day = SimpleDateFormat("EEEE", Locale.US).format(Date(epochMs)) + val timeStr = SimpleDateFormat("h:mm a", Locale.US).format(Date(epochMs)) + return "Resets $day $timeStr" + } + private fun formatTime(ms: Long) = SimpleDateFormat("h:mm a", Locale.US).format(Date(ms)) } diff --git a/app/src/main/java/me/khodak/claudeusage/MainActivity.kt b/app/src/main/java/me/khodak/claudeusage/MainActivity.kt index 7ec81cc..612c965 100644 --- a/app/src/main/java/me/khodak/claudeusage/MainActivity.kt +++ b/app/src/main/java/me/khodak/claudeusage/MainActivity.kt @@ -113,31 +113,28 @@ class MainActivity : AppCompatActivity() { else "Off-peak · ${peak.windowLabel}" - // ── Session (5-hour) bar + pace ────────────────────────────────────── - val sessionPct = data.progressPercent - val sessionPace = if (data.fiveHourUtilization >= 0f) - PaceCalc.compute(data.fiveHourUtilization, data.utilizationResetAtEpoch, PaceCalc.SESSION_WINDOW_MS) - else null + // ── Session (5-hour) bar — no pace marker ──────────────────────────── binding.barSession.setImageBitmap( - BarRenderer.render(sessionPct, sessionPace?.markerPct, SESSION_FILL, sessionPace?.tierColor) + BarRenderer.render(data.progressPercent, null, SESSION_FILL, null) ) - binding.tvSessionPace.text = paceSentence(data.fiveHourUtilization, sessionPace) - // ── Weekly (7-day) bar + pace ──────────────────────────────────────── + // ── Weekly (7-day) bar — single-color pace marker ──────────────────── if (data.weeklyUtilization >= 0f) { val wPct = data.weeklyUtilization.toInt() val weeklyPace = PaceCalc.compute(data.weeklyUtilization, data.weeklyResetAtEpoch, PaceCalc.WEEKLY_WINDOW_MS) binding.barWeekly.setImageBitmap( - BarRenderer.render(wPct, weeklyPace?.markerPct, WEEKLY_FILL, weeklyPace?.tierColor) + BarRenderer.render(wPct, weeklyPace?.markerPct, WEEKLY_FILL, if (weeklyPace != null) MARKER_COLOR else null) ) binding.tvWeeklyUsage.text = "$wPct% this week" - binding.tvWeeklyPace.text = paceSentence(data.weeklyUtilization, weeklyPace) } else { binding.barWeekly.setImageBitmap(BarRenderer.render(0, null, WEEKLY_FILL, null)) binding.tvWeeklyUsage.text = "—" - binding.tvWeeklyPace.text = "" } + // Pace text removed per design — bars carry the signal. + binding.tvSessionPace.visibility = View.GONE + binding.tvWeeklyPace.visibility = View.GONE + binding.tvUsage.text = when { data.fiveHourUtilization >= 0f -> { val pct = data.fiveHourUtilization.toInt() @@ -153,7 +150,7 @@ class MainActivity : AppCompatActivity() { } binding.tvReset.text = formatReset(data.effectiveResetEpoch) - binding.tvWeeklyReset.text = formatReset(data.weeklyResetAtEpoch) + binding.tvWeeklyReset.text = formatResetDay(data.weeklyResetAtEpoch) binding.tvUpdated.text = if (data.lastUpdated > 0) "Last updated: ${SimpleDateFormat("h:mm a, MMM d", Locale.US).format(Date(data.lastUpdated))}" else "" @@ -162,12 +159,6 @@ class MainActivity : AppCompatActivity() { binding.tvError.visibility = if (data.errorMessage.isNotBlank()) View.VISIBLE else View.GONE } - /** "20% over pace · will likely hit limit" — empty when no projection is available yet. */ - private fun paceSentence(usedPct: Float, pace: PaceCalc.Pace?): String { - if (pace == null) return "" - return "${PaceCalc.shortTag(usedPct, pace)} · ${pace.label}" - } - private fun formatReset(epochMs: Long): String { if (epochMs <= 0) return "" val now = System.currentTimeMillis() @@ -181,9 +172,19 @@ class MainActivity : AppCompatActivity() { } } + /** Weekly reset shown with the weekday name ("Resets Friday 3:00 PM"), never "tomorrow". */ + private fun formatResetDay(epochMs: Long): String { + if (epochMs <= 0) return "" + if (epochMs <= System.currentTimeMillis()) return "Resets soon" + val day = SimpleDateFormat("EEEE", Locale.US).format(Date(epochMs)) + val timeStr = SimpleDateFormat("h:mm a", Locale.US).format(Date(epochMs)) + return "Resets $day $timeStr" + } + companion object { private const val SESSION_FILL = 0xFFCC785C.toInt() private const val WEEKLY_FILL = 0xFF7B8FCC.toInt() + private const val MARKER_COLOR = 0xFFFFFFFF.toInt() // single-color weekly pace marker private const val PEAK_ON = 0xFFCC785C.toInt() private const val PEAK_OFF = 0xFF666666.toInt() } diff --git a/app/src/main/java/me/khodak/claudeusage/PaceCalc.kt b/app/src/main/java/me/khodak/claudeusage/PaceCalc.kt index 4f3dfa3..5961d2c 100644 --- a/app/src/main/java/me/khodak/claudeusage/PaceCalc.kt +++ b/app/src/main/java/me/khodak/claudeusage/PaceCalc.kt @@ -62,14 +62,4 @@ object PaceCalc { } return Pace(markerPct, projected, color, label) } - - /** Short tag for the widget reset line, e.g. "8% under pace" / "20% over pace" / "on pace". */ - fun shortTag(usedPct: Float, pace: Pace): String { - val delta = usedPct.toInt() - pace.markerPct - return when { - delta >= 2 -> "$delta% over pace" - delta <= -2 -> "${-delta}% under pace" - else -> "on pace" - } - } } diff --git a/releases/latest/claude-usage-widget.apk b/releases/latest/claude-usage-widget.apk index 803cc7f..0442ca5 100644 Binary files a/releases/latest/claude-usage-widget.apk and b/releases/latest/claude-usage-widget.apk differ