v1.11: pace markers + peak-hours burst icon

Add a pace tick to each usage bar showing where you should be to finish
at 100% by reset, color-coded by projected tier, plus a Claude burst icon
that lights up during Anthropic peak hours (5-11 AM PT, Mon-Fri). Bars now
rendered as bitmaps so the same renderer drives both the widget and the app.

New: PaceCalc, PeakHours, BarRenderer, ic_claude_burst drawable.
versionCode 12 / versionName 1.11. Includes rebuilt signed release APK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 02:31:10 +00:00
parent f7444a06eb
commit 838b10f2fd
12 changed files with 445 additions and 72 deletions
@@ -104,15 +104,38 @@ class MainActivity : AppCompatActivity() {
if (!loggedIn || data == null) return
binding.progressBar.progress = data.progressPercent
// ── Peak-hours row ───────────────────────────────────────────────────
val peak = PeakHours.state()
binding.imgPeak.setColorFilter(if (peak.active) PEAK_ON else PEAK_OFF)
binding.tvPeak.setTextColor(if (peak.active) PEAK_ON else 0xFFAAAAAA.toInt())
binding.tvPeak.text = if (peak.active)
"🔥 Peak hours — ${peak.endsInLabel} · ${peak.windowLabel}"
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
binding.barSession.setImageBitmap(
BarRenderer.render(sessionPct, sessionPace?.markerPct, SESSION_FILL, sessionPace?.tierColor)
)
binding.tvSessionPace.text = paceSentence(data.fiveHourUtilization, sessionPace)
// ── Weekly (7-day) bar + pace ────────────────────────────────────────
if (data.weeklyUtilization >= 0f) {
val wPct = data.weeklyUtilization.toInt()
binding.progressBarWeekly.progress = wPct
val weeklyPace = PaceCalc.compute(data.weeklyUtilization, data.weeklyResetAtEpoch, PaceCalc.WEEKLY_WINDOW_MS)
binding.barWeekly.setImageBitmap(
BarRenderer.render(wPct, weeklyPace?.markerPct, WEEKLY_FILL, weeklyPace?.tierColor)
)
binding.tvWeeklyUsage.text = "$wPct% this week"
binding.tvWeeklyPace.text = paceSentence(data.weeklyUtilization, weeklyPace)
} else {
binding.progressBarWeekly.progress = 0
binding.barWeekly.setImageBitmap(BarRenderer.render(0, null, WEEKLY_FILL, null))
binding.tvWeeklyUsage.text = ""
binding.tvWeeklyPace.text = ""
}
binding.tvUsage.text = when {
@@ -139,6 +162,12 @@ 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()
@@ -151,4 +180,11 @@ class MainActivity : AppCompatActivity() {
else -> "Resets ${SimpleDateFormat("EEE", Locale.US).format(Date(epochMs))} $timeStr"
}
}
companion object {
private const val SESSION_FILL = 0xFFCC785C.toInt()
private const val WEEKLY_FILL = 0xFF7B8FCC.toInt()
private const val PEAK_ON = 0xFFCC785C.toInt()
private const val PEAK_OFF = 0xFF666666.toInt()
}
}