Add app source and tests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
alvocool
2026-10-11 10:15:30 +03:00
parent 7e246255c2
commit 38881d1fe1
135 changed files with 21057 additions and 0 deletions

View 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;
}