Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ae0f466f50 | |||
| 1d89b2631c | |||
| b15dcf16d7 | |||
| d6d7daa30f |
@@ -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.
Reference in New Issue
Block a user