| 1 |
package io.oxygenhub.android.widget |
| 2 |
|
| 3 |
import android.graphics.Bitmap |
| 4 |
import android.graphics.Canvas |
| 5 |
import android.graphics.Paint |
| 6 |
import android.graphics.RectF |
| 7 |
import androidx.compose.ui.graphics.toArgb |
| 8 |
import io.oxygenhub.android.ui.theme.DarkBad |
| 9 |
import io.oxygenhub.android.ui.theme.DarkOk |
| 10 |
import io.oxygenhub.android.ui.theme.DarkWarn |
| 11 |
import io.oxygenhub.android.ui.theme.LightBad |
| 12 |
import io.oxygenhub.android.ui.theme.LightOk |
| 13 |
import io.oxygenhub.android.ui.theme.LightWarn |
| 14 |
|
| 15 |
/** |
| 16 |
* The widget's Flow health strip (#33): your last runs, oldest on the left, |
| 17 |
* a bar each in its outcome's colour, and how many of the finished ones |
| 18 |
* passed. Drawn as one picture, as a widget's row holds only ten children. |
| 19 |
*/ |
| 20 |
object HealthStrip { |
| 21 |
/** The kinds a run's status comes to. */ |
| 22 |
enum class Kind { Passed, Failed, Live, Other } |
| 23 |
|
| 24 |
fun kind(status: String): Kind = when (status) { |
| 25 |
"passed" -> Kind.Passed |
| 26 |
"failed", "error" -> Kind.Failed |
| 27 |
"running", "queued" -> Kind.Live |
| 28 |
else -> Kind.Other |
| 29 |
} |
| 30 |
|
| 31 |
/** "93% of 28 finished runs passed", or null with nothing finished to count. Cancelled runs do not count. */ |
| 32 |
fun summary(statuses: List<String>): String? { |
| 33 |
val kinds = statuses.map(::kind) |
| 34 |
val finished = kinds.count { it == Kind.Passed || it == Kind.Failed } |
| 35 |
if (finished == 0) return null |
| 36 |
val passed = kinds.count { it == Kind.Passed } |
| 37 |
val pct = (passed * 100 + finished / 2) / finished |
| 38 |
return if (finished == 1) "$pct% of the last finished run passed" else "$pct% of the last $finished finished runs passed" |
| 39 |
} |
| 40 |
|
| 41 |
/** The strip for [statuses] (newest first, as the hub lists them) at [width] × [height] pixels. */ |
| 42 |
fun render(statuses: List<String>, width: Int, height: Int, density: Float, dark: Boolean): Bitmap { |
| 43 |
val bitmap = Bitmap.createBitmap(width.coerceAtLeast(1), height.coerceAtLeast(1), Bitmap.Config.ARGB_8888) |
| 44 |
val canvas = Canvas(bitmap) |
| 45 |
val runs = statuses.take(MAX).reversed() |
| 46 |
if (runs.isEmpty()) return bitmap |
| 47 |
val gap = 2f * density |
| 48 |
val bar = ((width - gap * (MAX - 1)) / MAX).coerceAtLeast(1f) |
| 49 |
val paint = Paint(Paint.ANTI_ALIAS_FLAG) |
| 50 |
// Right-aligned, so the newest run is always at the right edge. |
| 51 |
var x = width - runs.size * bar - (runs.size - 1) * gap |
| 52 |
for (s in runs) { |
| 53 |
paint.color = colour(kind(s), dark) |
| 54 |
canvas.drawRoundRect(RectF(x, 0f, x + bar, height.toFloat()), 1.5f * density, 1.5f * density, paint) |
| 55 |
x += bar + gap |
| 56 |
} |
| 57 |
return bitmap |
| 58 |
} |
| 59 |
|
| 60 |
private fun colour(k: Kind, dark: Boolean): Int = when (k) { |
| 61 |
Kind.Passed -> (if (dark) DarkOk else LightOk).toArgb() |
| 62 |
Kind.Failed -> (if (dark) DarkBad else LightBad).toArgb() |
| 63 |
Kind.Live -> (if (dark) DarkWarn else LightWarn).toArgb() |
| 64 |
Kind.Other -> if (dark) 0x66FFFFFF else 0x55000000 |
| 65 |
} |
| 66 |
|
| 67 |
const val MAX = 30 |
| 68 |
} |