Add app source and tests
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
184
lib/Peach/designs/StatsDonut.dart
Normal file
184
lib/Peach/designs/StatsDonut.dart
Normal file
@@ -0,0 +1,184 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../about/internal/application/TextType.dart';
|
||||
|
||||
import '../utils/Colors.dart';
|
||||
import 'text/Text.dart';
|
||||
|
||||
/// Job-statistics style card: a donut gauge with the total in the centre, plus
|
||||
/// labelled progress bars underneath.
|
||||
class StatsDonut extends StatelessWidget {
|
||||
final int success;
|
||||
final int failed;
|
||||
final int reverted;
|
||||
const StatsDonut({
|
||||
super.key,
|
||||
required this.success,
|
||||
required this.failed,
|
||||
required this.reverted,
|
||||
});
|
||||
|
||||
int get _total => success + failed + reverted;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final jp = context.jp;
|
||||
final total = _total;
|
||||
double pct(int n) => total == 0 ? 0 : n / total;
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: PeachTheme.panel(context),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
text(
|
||||
'Deploy statistics',
|
||||
16,
|
||||
TextType.Bold,
|
||||
color: jp.text,
|
||||
weight: FontWeight.w800,
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
color: jp.surface2,
|
||||
borderRadius: BorderRadius.circular(9),
|
||||
),
|
||||
child: Icon(Icons.open_in_full, size: 14, color: jp.textDim),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Center(
|
||||
child: SizedBox(
|
||||
width: 176,
|
||||
height: 176,
|
||||
child: CustomPaint(
|
||||
painter: _DonutPainter(
|
||||
track: jp.surface2,
|
||||
segments: [
|
||||
(pct(success), PeachTheme.seed),
|
||||
(pct(failed), PeachTheme.danger),
|
||||
(pct(reverted), PeachTheme.slate),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
text(
|
||||
'$total',
|
||||
38,
|
||||
TextType.Bold,
|
||||
color: jp.text,
|
||||
height: 1,
|
||||
weight: FontWeight.w800,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
text(
|
||||
'Total deploys',
|
||||
12,
|
||||
TextType.Regular,
|
||||
color: jp.textFaint,
|
||||
weight: FontWeight.w400,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
_bar(jp, 'Successful', success, pct(success), PeachTheme.seed),
|
||||
const SizedBox(height: 14),
|
||||
_bar(jp, 'Failed', failed, pct(failed), PeachTheme.danger),
|
||||
const SizedBox(height: 14),
|
||||
_bar(jp, 'Reverted', reverted, pct(reverted), PeachTheme.slate),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _bar(JpColors jp, String label, int n, double frac, Color color) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
text(
|
||||
label,
|
||||
13,
|
||||
TextType.Regular,
|
||||
color: jp.textDim,
|
||||
weight: FontWeight.w600,
|
||||
),
|
||||
const Spacer(),
|
||||
text(
|
||||
'${(frac * 100).round()}%',
|
||||
13,
|
||||
TextType.Regular,
|
||||
color: jp.text,
|
||||
weight: FontWeight.w800,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(height: 8, color: jp.surface2),
|
||||
FractionallySizedBox(
|
||||
widthFactor: frac.clamp(0.0, 1.0),
|
||||
child: Container(height: 8, color: color),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DonutPainter extends CustomPainter {
|
||||
final List<(double, Color)> segments;
|
||||
final Color track;
|
||||
_DonutPainter({required this.segments, required this.track});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
const stroke = 18.0;
|
||||
final rect =
|
||||
Offset(stroke / 2, stroke / 2) &
|
||||
Size(size.width - stroke, size.height - stroke);
|
||||
final trackPaint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = stroke
|
||||
..color = track;
|
||||
canvas.drawArc(rect, 0, 2 * math.pi, false, trackPaint);
|
||||
|
||||
const gap = 0.06;
|
||||
var start = -math.pi / 2;
|
||||
for (final (frac, color) in segments) {
|
||||
if (frac <= 0) continue;
|
||||
final full = frac * 2 * math.pi;
|
||||
final sweep = (full - gap).clamp(0.02, 2 * math.pi);
|
||||
final p = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = stroke
|
||||
..strokeCap = StrokeCap.round
|
||||
..color = color;
|
||||
canvas.drawArc(rect, start + gap / 2, sweep, false, p);
|
||||
start += full;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_DonutPainter old) =>
|
||||
old.segments != segments || old.track != track;
|
||||
}
|
||||
Reference in New Issue
Block a user