Initial release: Claude Pro usage widget for Android
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
package me.khodak.claudeusage
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import me.khodak.claudeusage.data.PreferencesManager
|
||||
import me.khodak.claudeusage.data.UsageData
|
||||
import me.khodak.claudeusage.databinding.ActivityMainBinding
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var prefs: PreferencesManager
|
||||
private lateinit var repo: UsageRepository
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
prefs = PreferencesManager(this)
|
||||
repo = UsageRepository(prefs)
|
||||
|
||||
binding.btnLogin.setOnClickListener {
|
||||
startActivity(Intent(this, LoginActivity::class.java))
|
||||
}
|
||||
|
||||
// If already logged in, go straight to logged-in state without re-opening login
|
||||
if (prefs.isLoggedIn()) {
|
||||
updateUI(prefs.getUsageData())
|
||||
}
|
||||
|
||||
binding.btnLogout.setOnClickListener {
|
||||
prefs.clearSession()
|
||||
updateUI(null)
|
||||
}
|
||||
|
||||
binding.btnRefresh.setOnClickListener {
|
||||
refreshUsage()
|
||||
}
|
||||
|
||||
binding.tvWidgetHint.setOnClickListener {
|
||||
startActivity(Intent(Intent.ACTION_MAIN).apply {
|
||||
addCategory(Intent.CATEGORY_HOME)
|
||||
})
|
||||
}
|
||||
|
||||
binding.btnDebug.setOnClickListener {
|
||||
if (binding.tvDebugInfo.visibility == android.view.View.GONE) {
|
||||
binding.tvDebugInfo.text = repo.lastDebugInfo.ifBlank { "No debug info yet — tap Refresh first" }
|
||||
binding.tvDebugInfo.visibility = android.view.View.VISIBLE
|
||||
binding.btnDebug.text = "Hide API Debug"
|
||||
} else {
|
||||
binding.tvDebugInfo.visibility = android.view.View.GONE
|
||||
binding.btnDebug.text = "Show API Debug"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val cached = prefs.getUsageData()
|
||||
updateUI(cached)
|
||||
if (prefs.isLoggedIn()) {
|
||||
refreshUsage()
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshUsage() {
|
||||
binding.btnRefresh.isEnabled = false
|
||||
binding.progressIndicator.visibility = View.VISIBLE
|
||||
lifecycleScope.launch {
|
||||
val data = try {
|
||||
repo.fetchUsage()
|
||||
} catch (e: Exception) {
|
||||
prefs.getUsageData()?.copy(errorMessage = "Network error")
|
||||
?: UsageData(errorMessage = "Network error")
|
||||
}
|
||||
prefs.saveUsageData(data)
|
||||
updateUI(data)
|
||||
if (binding.tvDebugInfo.visibility == View.VISIBLE) {
|
||||
binding.tvDebugInfo.text = repo.lastDebugInfo
|
||||
}
|
||||
binding.btnRefresh.isEnabled = true
|
||||
binding.progressIndicator.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUI(data: UsageData?) {
|
||||
val loggedIn = prefs.isLoggedIn()
|
||||
|
||||
binding.layoutLoggedOut.visibility = if (loggedIn) View.GONE else View.VISIBLE
|
||||
binding.layoutLoggedIn.visibility = if (loggedIn) View.VISIBLE else View.GONE
|
||||
|
||||
if (!loggedIn || data == null) return
|
||||
|
||||
binding.progressBar.progress = data.progressPercent
|
||||
|
||||
if (data.weeklyUtilization >= 0f) {
|
||||
val wPct = data.weeklyUtilization.toInt()
|
||||
binding.progressBarWeekly.progress = wPct
|
||||
binding.tvWeeklyUsage.text = "$wPct% this week"
|
||||
} else {
|
||||
binding.progressBarWeekly.progress = 0
|
||||
binding.tvWeeklyUsage.text = "—"
|
||||
}
|
||||
|
||||
binding.tvUsage.text = when {
|
||||
data.fiveHourUtilization >= 0f -> {
|
||||
val pct = data.fiveHourUtilization.toInt()
|
||||
"$pct% of limit used"
|
||||
}
|
||||
data.messagesLimit > 0 && data.effectiveUsed >= 0 ->
|
||||
"${data.effectiveUsed} of ${data.messagesLimit} messages used"
|
||||
data.effectiveRemaining >= 0 && data.messagesLimit > 0 ->
|
||||
"${data.effectiveRemaining} of ${data.messagesLimit} remaining"
|
||||
data.effectiveRemaining >= 0 ->
|
||||
"${data.effectiveRemaining} messages remaining"
|
||||
else -> "Usage data unavailable"
|
||||
}
|
||||
|
||||
binding.tvReset.text = formatReset(data.effectiveResetEpoch)
|
||||
binding.tvWeeklyReset.text = formatReset(data.weeklyResetAtEpoch)
|
||||
binding.tvUpdated.text = if (data.lastUpdated > 0)
|
||||
"Last updated: ${SimpleDateFormat("h:mm a, MMM d", Locale.US).format(Date(data.lastUpdated))}"
|
||||
else ""
|
||||
|
||||
binding.tvError.text = data.errorMessage
|
||||
binding.tvError.visibility = if (data.errorMessage.isNotBlank()) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun formatReset(epochMs: Long): String {
|
||||
if (epochMs <= 0) return ""
|
||||
val now = System.currentTimeMillis()
|
||||
if (epochMs <= now) return "Resets soon"
|
||||
val diffH = TimeUnit.MILLISECONDS.toHours(epochMs - now)
|
||||
val timeStr = SimpleDateFormat("h:mm a", Locale.US).format(Date(epochMs))
|
||||
return when {
|
||||
diffH < 24 -> "Resets at $timeStr"
|
||||
diffH < 48 -> "Resets tomorrow $timeStr"
|
||||
else -> "Resets ${SimpleDateFormat("EEE", Locale.US).format(Date(epochMs))} $timeStr"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user