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"
minSdk = 26
targetSdk = 34
versionCode = 12
versionName = "1.11"
versionCode = 14
versionName = "1.13"
}
signingConfigs {
@@ -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,14 +42,16 @@ object BarRenderer {
canvas.drawRoundRect(fill, cornerPx, cornerPx, paint)
}
// Pace tick — "where you should be right now"
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 tickW = (wPx * 0.012f).coerceIn(3f, 7f)
val tickW = (wPx * 0.016f).coerceIn(6f, 10f)
var x = wPx * m / 100f
x = x.coerceIn(tickW / 2f, wPx - tickW / 2f)
paint.color = tierColor
canvas.drawRect(x - tickW / 2f, 0f, x + tickW / 2f, hPx.toFloat(), paint)
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
@@ -104,11 +104,9 @@ 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.setTextViewText(R.id.tv_session_label,
if (pace != null) PaceCalc.shortTag(apiData.fiveHourUtilization, pace) else formatReset(apiData.utilizationResetAtEpoch))
v.setImageViewBitmap(R.id.bar_session, BarRenderer.render(pct, null, SESSION_FILL, null))
v.setTextViewText(R.id.tv_session_label, formatReset(apiData.utilizationResetAtEpoch))
}
hasApiMessages -> {
val rem = apiData!!.effectiveRemaining
@@ -136,9 +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,
if (pace != null) PaceCalc.shortTag(apiData.weeklyUtilization, pace) else 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")
@@ -184,10 +181,9 @@ 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.setTextViewText(R.id.tv_session_label, resetWithPace(apiData.utilizationResetAtEpoch, apiData.fiveHourUtilization, pace))
v.setImageViewBitmap(R.id.bar_session, BarRenderer.render(pct, null, SESSION_FILL, null))
v.setTextViewText(R.id.tv_session_label, formatReset(apiData.utilizationResetAtEpoch))
}
hasApiMessages -> {
val rem = apiData!!.effectiveRemaining
@@ -218,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, resetWithPace(apiData.weeklyResetAtEpoch, apiData.weeklyUtilization, pace))
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")
@@ -249,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) {
@@ -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 {
if (epochMs <= 0) return ""
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) =
SimpleDateFormat("h:mm a", Locale.US).format(Date(ms))
}
@@ -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()
}
@@ -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"
}
}
}
Binary file not shown.
Binary file not shown.