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:
@@ -87,14 +87,15 @@ class ClaudeUsageWidget : AppWidgetProvider() {
|
||||
|
||||
private fun buildSmallViews(context: Context, prefs: PreferencesManager, apiData: UsageData?): RemoteViews {
|
||||
val v = RemoteViews(context.packageName, R.layout.widget_layout_small)
|
||||
applyPeak(v, showText = false)
|
||||
if (!prefs.isLoggedIn()) {
|
||||
v.setTextViewText(R.id.tv_session_value, "—")
|
||||
v.setTextViewText(R.id.tv_session_label, "Not signed in")
|
||||
v.setTextViewText(R.id.tv_weekly_value, "—")
|
||||
v.setTextViewText(R.id.tv_weekly_label, "")
|
||||
v.setTextViewText(R.id.tv_status, "")
|
||||
v.setProgressBar(R.id.progress_bar, 100, 0, false)
|
||||
v.setProgressBar(R.id.progress_bar_weekly, 100, 0, false)
|
||||
v.setImageViewBitmap(R.id.bar_session, BarRenderer.render(0, null, SESSION_FILL, null))
|
||||
v.setImageViewBitmap(R.id.bar_weekly, BarRenderer.render(0, null, WEEKLY_FILL, null))
|
||||
return v
|
||||
}
|
||||
val hasUtilization = apiData != null && apiData.fiveHourUtilization >= 0f
|
||||
@@ -103,15 +104,17 @@ 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.setProgressBar(R.id.progress_bar, 100, pct, false)
|
||||
v.setTextViewText(R.id.tv_session_label, formatReset(apiData.utilizationResetAtEpoch))
|
||||
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))
|
||||
}
|
||||
hasApiMessages -> {
|
||||
val rem = apiData!!.effectiveRemaining
|
||||
val lim = apiData.messagesLimit
|
||||
v.setTextViewText(R.id.tv_session_value, "$rem/$lim")
|
||||
v.setProgressBar(R.id.progress_bar, 100, apiData.progressPercent, false)
|
||||
v.setImageViewBitmap(R.id.bar_session, BarRenderer.render(apiData.progressPercent, null, SESSION_FILL, null))
|
||||
v.setTextViewText(R.id.tv_session_label, formatReset(apiData.resetAtEpoch))
|
||||
}
|
||||
sessionStart > 0 -> {
|
||||
@@ -119,25 +122,27 @@ class ClaudeUsageWidget : AppWidgetProvider() {
|
||||
val h = TimeUnit.MILLISECONDS.toHours(elapsedMs)
|
||||
val m = TimeUnit.MILLISECONDS.toMinutes(elapsedMs) % 60
|
||||
v.setTextViewText(R.id.tv_session_value, if (h > 0) "${h}h${m}m" else "${m}m")
|
||||
v.setProgressBar(R.id.progress_bar, 100, 0, false)
|
||||
v.setImageViewBitmap(R.id.bar_session, BarRenderer.render(0, null, SESSION_FILL, null))
|
||||
v.setTextViewText(R.id.tv_session_label, "active")
|
||||
}
|
||||
else -> {
|
||||
v.setTextViewText(R.id.tv_session_value, "—")
|
||||
v.setProgressBar(R.id.progress_bar, 100, 0, false)
|
||||
v.setImageViewBitmap(R.id.bar_session, BarRenderer.render(0, null, SESSION_FILL, null))
|
||||
v.setTextViewText(R.id.tv_session_label, "")
|
||||
}
|
||||
}
|
||||
val hasWeekly = apiData != null && apiData.weeklyUtilization >= 0f
|
||||
if (hasWeekly) {
|
||||
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.setProgressBar(R.id.progress_bar_weekly, 100, wPct, false)
|
||||
v.setTextViewText(R.id.tv_weekly_label, formatReset(apiData.weeklyResetAtEpoch))
|
||||
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))
|
||||
} else {
|
||||
val weeklyDays = Integer.bitCount(prefs.getWeeklyMask())
|
||||
v.setTextViewText(R.id.tv_weekly_value, "${weeklyDays}d")
|
||||
v.setProgressBar(R.id.progress_bar_weekly, 100, 0, false)
|
||||
v.setImageViewBitmap(R.id.bar_weekly, BarRenderer.render(0, null, WEEKLY_FILL, null))
|
||||
v.setTextViewText(R.id.tv_weekly_label, "")
|
||||
}
|
||||
val resetEpoch = apiData?.effectiveResetEpoch ?: -1L
|
||||
@@ -157,6 +162,7 @@ class ClaudeUsageWidget : AppWidgetProvider() {
|
||||
|
||||
private fun buildViews(context: Context, prefs: PreferencesManager, apiData: UsageData?): RemoteViews {
|
||||
val v = RemoteViews(context.packageName, R.layout.widget_layout)
|
||||
applyPeak(v, showText = true)
|
||||
|
||||
// ── Not logged in ────────────────────────────────────────────────
|
||||
if (!prefs.isLoggedIn()) {
|
||||
@@ -165,7 +171,8 @@ class ClaudeUsageWidget : AppWidgetProvider() {
|
||||
v.setTextViewText(R.id.tv_weekly_value, "—")
|
||||
v.setTextViewText(R.id.tv_weekly_label, "Open app to sign in")
|
||||
v.setTextViewText(R.id.tv_status, "")
|
||||
v.setProgressBar(R.id.progress_bar, 100, 0, false)
|
||||
v.setImageViewBitmap(R.id.bar_session, BarRenderer.render(0, null, SESSION_FILL, null))
|
||||
v.setImageViewBitmap(R.id.bar_weekly, BarRenderer.render(0, null, WEEKLY_FILL, null))
|
||||
return v
|
||||
}
|
||||
|
||||
@@ -177,15 +184,16 @@ 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.setProgressBar(R.id.progress_bar, 100, pct, false)
|
||||
v.setTextViewText(R.id.tv_session_label, formatReset(apiData.utilizationResetAtEpoch))
|
||||
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))
|
||||
}
|
||||
hasApiMessages -> {
|
||||
val rem = apiData!!.effectiveRemaining
|
||||
val lim = apiData.messagesLimit
|
||||
v.setTextViewText(R.id.tv_session_value, "$rem / $lim")
|
||||
v.setProgressBar(R.id.progress_bar, 100, apiData.progressPercent, false)
|
||||
v.setImageViewBitmap(R.id.bar_session, BarRenderer.render(apiData.progressPercent, null, SESSION_FILL, null))
|
||||
v.setTextViewText(R.id.tv_session_label, formatReset(apiData.resetAtEpoch))
|
||||
}
|
||||
sessionStart > 0 -> {
|
||||
@@ -194,12 +202,12 @@ class ClaudeUsageWidget : AppWidgetProvider() {
|
||||
val m = TimeUnit.MILLISECONDS.toMinutes(elapsedMs) % 60
|
||||
val display = if (h > 0) "${h}h ${m}m" else if (m > 0) "${m}m" else "Just started"
|
||||
v.setTextViewText(R.id.tv_session_value, display)
|
||||
v.setProgressBar(R.id.progress_bar, 100, 0, false)
|
||||
v.setImageViewBitmap(R.id.bar_session, BarRenderer.render(0, null, SESSION_FILL, null))
|
||||
v.setTextViewText(R.id.tv_session_label, "session active")
|
||||
}
|
||||
else -> {
|
||||
v.setTextViewText(R.id.tv_session_value, "—")
|
||||
v.setProgressBar(R.id.progress_bar, 100, 0, false)
|
||||
v.setImageViewBitmap(R.id.bar_session, BarRenderer.render(0, null, SESSION_FILL, null))
|
||||
v.setTextViewText(R.id.tv_session_label, "")
|
||||
}
|
||||
}
|
||||
@@ -208,13 +216,14 @@ class ClaudeUsageWidget : AppWidgetProvider() {
|
||||
val hasWeekly = apiData != null && apiData.weeklyUtilization >= 0f
|
||||
if (hasWeekly) {
|
||||
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.setProgressBar(R.id.progress_bar_weekly, 100, wPct, false)
|
||||
v.setTextViewText(R.id.tv_weekly_label, formatReset(apiData.weeklyResetAtEpoch))
|
||||
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))
|
||||
} else {
|
||||
val weeklyDays = Integer.bitCount(prefs.getWeeklyMask())
|
||||
v.setTextViewText(R.id.tv_weekly_value, "$weeklyDays d")
|
||||
v.setProgressBar(R.id.progress_bar_weekly, 100, 0, false)
|
||||
v.setImageViewBitmap(R.id.bar_weekly, BarRenderer.render(0, null, WEEKLY_FILL, null))
|
||||
v.setTextViewText(R.id.tv_weekly_label, "active this week")
|
||||
}
|
||||
|
||||
@@ -238,6 +247,27 @@ class ClaudeUsageWidget : AppWidgetProvider() {
|
||||
return v
|
||||
}
|
||||
|
||||
private const val SESSION_FILL = 0xFFCC785C.toInt()
|
||||
private const val WEEKLY_FILL = 0xFF7B8FCC.toInt()
|
||||
|
||||
/** Tints the header burst icon and (optionally) the PEAK text by current peak state. */
|
||||
private fun applyPeak(v: RemoteViews, showText: Boolean) {
|
||||
val peak = PeakHours.isPeak()
|
||||
v.setInt(R.id.img_peak, "setColorFilter",
|
||||
if (peak) 0xFFCC785C.toInt() else 0xFF666666.toInt())
|
||||
if (showText) {
|
||||
v.setTextViewText(R.id.tv_peak, if (peak) "🔥 PEAK" else "")
|
||||
}
|
||||
}
|
||||
|
||||
/** "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()
|
||||
|
||||
Reference in New Issue
Block a user