4 Commits

Author SHA1 Message Date
amir ae0f466f50 releases/latest: add v1.13 source zip
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 05:53:50 +00:00
amir 1d89b2631c v1.13: drop session marker, single-color weekly marker, weekday reset
- Remove the pace marker from the 5-hour (session) bar entirely.
- Weekly bar marker is now a single color (white), no green→purple tiers.
- Marker is a clean rounded tick; removed the white-halo/tier styling.
- Remove the '% over/under pace' text everywhere (widget + app).
- Weekly reset label now shows the weekday ('Resets Friday 3:00 PM'),
  never 'tomorrow'.

versionCode 14 / versionName 1.13. Includes rebuilt signed release APK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 05:53:50 +00:00
amir b15dcf16d7 releases/latest: add v1.12 source zip
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 02:42:45 +00:00
amir d6d7daa30f v1.12: restore reset-time labels, bolder pace tick
Fix v1.11 regression where the pace tag overwrote the reset-time label
(gone entirely on the small widget). The widget reset lines now show the
actual reset time again; pace is conveyed by the bar tick.

Make the pace tick more prominent: wider core + white halo so it stands
out against any fill color.

versionCode 13 / versionName 1.12. Includes rebuilt signed release APK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 02:42:45 +00:00
7 changed files with 47 additions and 56 deletions
+2 -2
View File
@@ -11,8 +11,8 @@ android {
applicationId = "me.khodak.claudeusage" applicationId = "me.khodak.claudeusage"
minSdk = 26 minSdk = 26
targetSdk = 34 targetSdk = 34
versionCode = 12 versionCode = 14
versionName = "1.11" versionName = "1.13"
} }
signingConfigs { signingConfigs {
@@ -19,7 +19,7 @@ object BarRenderer {
usedPct: Int, usedPct: Int,
markerPct: Int?, markerPct: Int?,
fillColor: Int, fillColor: Int,
tierColor: Int?, markerColor: Int?,
wPx: Int = 500, wPx: Int = 500,
hPx: Int = 14, hPx: Int = 14,
cornerPx: Float = 7f cornerPx: Float = 7f
@@ -42,14 +42,16 @@ object BarRenderer {
canvas.drawRoundRect(fill, cornerPx, cornerPx, paint) canvas.drawRoundRect(fill, cornerPx, cornerPx, paint)
} }
// Pace tick — "where you should be right now" // Pace marker — a single clean tick showing "where you should be right now".
if (markerPct != null && tierColor != null) { // One color (no tiers); rounded ends to match the bar.
if (markerPct != null && markerColor != null) {
val m = markerPct.coerceIn(0, 100) val m = markerPct.coerceIn(0, 100)
val tickW = (wPx * 0.012f).coerceIn(3f, 7f) val tickW = (wPx * 0.016f).coerceIn(6f, 10f)
var x = wPx * m / 100f var x = wPx * m / 100f
x = x.coerceIn(tickW / 2f, wPx - tickW / 2f) x = x.coerceIn(tickW / 2f, wPx - tickW / 2f)
paint.color = tierColor paint.color = markerColor
canvas.drawRect(x - tickW / 2f, 0f, x + tickW / 2f, hPx.toFloat(), paint) val tick = RectF(x - tickW / 2f, 0f, x + tickW / 2f, hPx.toFloat())
canvas.drawRoundRect(tick, tickW / 2f, tickW / 2f, paint)
} }
return bmp return bmp
@@ -104,11 +104,9 @@ class ClaudeUsageWidget : AppWidgetProvider() {
when { when {
hasUtilization -> { hasUtilization -> {
val pct = apiData!!.fiveHourUtilization.toInt() 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.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, v.setTextViewText(R.id.tv_session_label, formatReset(apiData.utilizationResetAtEpoch))
if (pace != null) PaceCalc.shortTag(apiData.fiveHourUtilization, pace) else formatReset(apiData.utilizationResetAtEpoch))
} }
hasApiMessages -> { hasApiMessages -> {
val rem = apiData!!.effectiveRemaining val rem = apiData!!.effectiveRemaining
@@ -136,9 +134,8 @@ class ClaudeUsageWidget : AppWidgetProvider() {
val wPct = apiData!!.weeklyUtilization.toInt() val wPct = apiData!!.weeklyUtilization.toInt()
val pace = PaceCalc.compute(apiData.weeklyUtilization, apiData.weeklyResetAtEpoch, PaceCalc.WEEKLY_WINDOW_MS) val pace = PaceCalc.compute(apiData.weeklyUtilization, apiData.weeklyResetAtEpoch, PaceCalc.WEEKLY_WINDOW_MS)
v.setTextViewText(R.id.tv_weekly_value, "$wPct%") v.setTextViewText(R.id.tv_weekly_value, "$wPct%")
v.setImageViewBitmap(R.id.bar_weekly, BarRenderer.render(wPct, pace?.markerPct, WEEKLY_FILL, pace?.tierColor)) 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, v.setTextViewText(R.id.tv_weekly_label, formatResetDay(apiData.weeklyResetAtEpoch))
if (pace != null) PaceCalc.shortTag(apiData.weeklyUtilization, pace) else formatReset(apiData.weeklyResetAtEpoch))
} else { } else {
val weeklyDays = Integer.bitCount(prefs.getWeeklyMask()) val weeklyDays = Integer.bitCount(prefs.getWeeklyMask())
v.setTextViewText(R.id.tv_weekly_value, "${weeklyDays}d") v.setTextViewText(R.id.tv_weekly_value, "${weeklyDays}d")
@@ -184,10 +181,9 @@ class ClaudeUsageWidget : AppWidgetProvider() {
when { when {
hasUtilization -> { hasUtilization -> {
val pct = apiData!!.fiveHourUtilization.toInt() 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.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, resetWithPace(apiData.utilizationResetAtEpoch, apiData.fiveHourUtilization, pace)) v.setTextViewText(R.id.tv_session_label, formatReset(apiData.utilizationResetAtEpoch))
} }
hasApiMessages -> { hasApiMessages -> {
val rem = apiData!!.effectiveRemaining val rem = apiData!!.effectiveRemaining
@@ -218,8 +214,8 @@ class ClaudeUsageWidget : AppWidgetProvider() {
val wPct = apiData!!.weeklyUtilization.toInt() val wPct = apiData!!.weeklyUtilization.toInt()
val pace = PaceCalc.compute(apiData.weeklyUtilization, apiData.weeklyResetAtEpoch, PaceCalc.WEEKLY_WINDOW_MS) val pace = PaceCalc.compute(apiData.weeklyUtilization, apiData.weeklyResetAtEpoch, PaceCalc.WEEKLY_WINDOW_MS)
v.setTextViewText(R.id.tv_weekly_value, "$wPct%") v.setTextViewText(R.id.tv_weekly_value, "$wPct%")
v.setImageViewBitmap(R.id.bar_weekly, BarRenderer.render(wPct, pace?.markerPct, WEEKLY_FILL, pace?.tierColor)) 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, resetWithPace(apiData.weeklyResetAtEpoch, apiData.weeklyUtilization, pace)) v.setTextViewText(R.id.tv_weekly_label, formatResetDay(apiData.weeklyResetAtEpoch))
} else { } else {
val weeklyDays = Integer.bitCount(prefs.getWeeklyMask()) val weeklyDays = Integer.bitCount(prefs.getWeeklyMask())
v.setTextViewText(R.id.tv_weekly_value, "$weeklyDays d") v.setTextViewText(R.id.tv_weekly_value, "$weeklyDays d")
@@ -249,6 +245,7 @@ class ClaudeUsageWidget : AppWidgetProvider() {
private const val SESSION_FILL = 0xFFCC785C.toInt() private const val SESSION_FILL = 0xFFCC785C.toInt()
private const val WEEKLY_FILL = 0xFF7B8FCC.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. */ /** Tints the header burst icon and (optionally) the PEAK text by current peak state. */
private fun applyPeak(v: RemoteViews, showText: Boolean) { private fun applyPeak(v: RemoteViews, showText: Boolean) {
@@ -260,14 +257,6 @@ class ClaudeUsageWidget : AppWidgetProvider() {
} }
} }
/** "Resets at 3:00 PM · 8% under pace" — reset line with the pace tag appended. */
private fun resetWithPace(resetEpoch: Long, usedPct: Float, pace: PaceCalc.Pace?): CharSequence {
val reset = formatReset(resetEpoch)
if (pace == null) return reset
val tag = PaceCalc.shortTag(usedPct, pace)
return if (reset.isBlank()) tag else "$reset · $tag"
}
private fun formatReset(epochMs: Long): String { private fun formatReset(epochMs: Long): String {
if (epochMs <= 0) return "" if (epochMs <= 0) return ""
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
@@ -281,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) = private fun formatTime(ms: Long) =
SimpleDateFormat("h:mm a", Locale.US).format(Date(ms)) SimpleDateFormat("h:mm a", Locale.US).format(Date(ms))
} }
@@ -113,31 +113,28 @@ class MainActivity : AppCompatActivity() {
else else
"Off-peak · ${peak.windowLabel}" "Off-peak · ${peak.windowLabel}"
// ── Session (5-hour) bar + pace ────────────────────────────────────── // ── Session (5-hour) bar — no pace marker ────────────────────────────
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( 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) { if (data.weeklyUtilization >= 0f) {
val wPct = data.weeklyUtilization.toInt() val wPct = data.weeklyUtilization.toInt()
val weeklyPace = PaceCalc.compute(data.weeklyUtilization, data.weeklyResetAtEpoch, PaceCalc.WEEKLY_WINDOW_MS) val weeklyPace = PaceCalc.compute(data.weeklyUtilization, data.weeklyResetAtEpoch, PaceCalc.WEEKLY_WINDOW_MS)
binding.barWeekly.setImageBitmap( 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.tvWeeklyUsage.text = "$wPct% this week"
binding.tvWeeklyPace.text = paceSentence(data.weeklyUtilization, weeklyPace)
} else { } else {
binding.barWeekly.setImageBitmap(BarRenderer.render(0, null, WEEKLY_FILL, null)) binding.barWeekly.setImageBitmap(BarRenderer.render(0, null, WEEKLY_FILL, null))
binding.tvWeeklyUsage.text = "" 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 { binding.tvUsage.text = when {
data.fiveHourUtilization >= 0f -> { data.fiveHourUtilization >= 0f -> {
val pct = data.fiveHourUtilization.toInt() val pct = data.fiveHourUtilization.toInt()
@@ -153,7 +150,7 @@ class MainActivity : AppCompatActivity() {
} }
binding.tvReset.text = formatReset(data.effectiveResetEpoch) 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) binding.tvUpdated.text = if (data.lastUpdated > 0)
"Last updated: ${SimpleDateFormat("h:mm a, MMM d", Locale.US).format(Date(data.lastUpdated))}" "Last updated: ${SimpleDateFormat("h:mm a, MMM d", Locale.US).format(Date(data.lastUpdated))}"
else "" else ""
@@ -162,12 +159,6 @@ class MainActivity : AppCompatActivity() {
binding.tvError.visibility = if (data.errorMessage.isNotBlank()) View.VISIBLE else View.GONE 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 { private fun formatReset(epochMs: Long): String {
if (epochMs <= 0) return "" if (epochMs <= 0) return ""
val now = System.currentTimeMillis() 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 { companion object {
private const val SESSION_FILL = 0xFFCC785C.toInt() private const val SESSION_FILL = 0xFFCC785C.toInt()
private const val WEEKLY_FILL = 0xFF7B8FCC.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_ON = 0xFFCC785C.toInt()
private const val PEAK_OFF = 0xFF666666.toInt() private const val PEAK_OFF = 0xFF666666.toInt()
} }
@@ -62,14 +62,4 @@ object PaceCalc {
} }
return Pace(markerPct, projected, color, label) 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"
}
}
} }
Binary file not shown.
Binary file not shown.