Sumber yang diterbitkan
Muat turun ZIP sumber
CH-J SM Mobile
Layari direktori dan fail untuk keluaran aplikasi tertentu.
CH-J / SM Mobile
175 fail
sm-mobile-20261016T185235Z-8e93ecc1b59a
Ditambah: · Dibina:
1
import 'dart:math' as math;2
import 'package:flutter/material.dart';3
import 'package:intl/intl.dart' show DateFormat, NumberFormat;4
import '../l10n/strings.dart';5
import '../services/net/measure.dart';6
import 'adaptive.dart';7
import 'theme.dart';9
/// Series colors: IPv4 blue, IPv6 violet, failures red.10
Color familyColor(Family f) => f == Family.v4 ? Brand.blue : Brand.violet;12
TextStyle mutedStyle(BuildContext context) => TextStyle(13
color: Theme.of(context).colorScheme.onSurfaceVariant,14
fontSize: 12,15
);17
const tabular = [FontFeature.tabularFigures()];19
String formatMs(BuildContext context, double v) =>20
NumberFormat.decimalPatternDigits(21
locale: Localizations.localeOf(context).toString(),22
decimalDigits: v < 10 ? 2 : 1,23
).format(v);25
String formatPercent(BuildContext context, double v) =>26
'${NumberFormat.decimalPatternDigits(locale: Localizations.localeOf(context).toString(), decimalDigits: 1).format(v)} %';28
String formatElapsed(Duration d) {29
final h = d.inHours, m = d.inMinutes % 60, s = d.inSeconds % 60;30
String two(int v) => v.toString().padLeft(2, '0');31
return h > 0 ? '$h:${two(m)}:${two(s)}' : '${two(m)}:${two(s)}';32
}34
String outcomeKey(Outcome o) => switch (o) {35
Outcome.ok => 'outcomeOk',36
Outcome.timeout => 'outcomeTimeout',37
Outcome.refused => 'outcomeRefused',38
Outcome.unreachable => 'outcomeUnreachable',39
Outcome.ttlExceeded => 'outcomeTtlExceeded',40
Outcome.error => 'outcomeError',41
};43
/// A labelled number.44
class Stat extends StatelessWidget {45
final String label, value;46
const Stat(this.label, this.value, {super.key});47
@override48
Widget build(BuildContext context) => Column(49
crossAxisAlignment: CrossAxisAlignment.start,50
children: [51
Text(label, style: mutedStyle(context)),52
Text(53
value,54
style: Theme.of(55
context,56
).textTheme.titleMedium?.copyWith(fontFeatures: tabular),57
),58
],59
);60
}62
/// The statistics of one series. RTT figures are shown only when at least63
/// one attempt succeeded.64
class StatsGrid extends StatelessWidget {65
final RttStats stats;66
const StatsGrid(this.stats, {super.key});67
@override68
Widget build(BuildContext context) {69
String ms(double? v) => v == null ? '—' : '${formatMs(context, v)} ms';70
return AdaptiveGrid(71
minTileWidth: 110,72
maxColumns: 6,73
spacing: 12,74
children: [75
Stat(context.tr('attempts'), '${stats.attempts}'),76
Stat(context.tr('successful'), '${stats.successes}'),77
Stat(context.tr('failedCount'), '${stats.failures}'),78
Stat(79
context.tr('successRate'),80
stats.attempts == 081
? '—'82
: formatPercent(context, stats.successPercent),83
),84
Stat(context.tr('rttMin'), ms(stats.min)),85
Stat(context.tr('rttAvg'), ms(stats.mean)),86
Stat(context.tr('rttMax'), ms(stats.max)),87
Stat(context.tr('rttMedian'), ms(stats.median)),88
Stat(context.tr('rttStddev'), ms(stats.stddev)),89
Stat(context.tr('jitter'), ms(stats.jitter)),90
Stat('P95', ms(stats.p95)),91
Stat(context.tr('elapsedTime'), formatElapsed(stats.elapsed)),92
],93
);94
}95
}97
class ChartSeries {98
final String label;99
final Color color;100
final List<Sample> samples;101
const ChartSeries(this.label, this.color, this.samples);102
}104
/// RTT over time for one or more series. Failed attempts are drawn as red105
/// marks along the top edge, not as invented RTT values.106
class RttChart extends StatelessWidget {107
final List<ChartSeries> series;108
final double height;109
const RttChart(this.series, {this.height = 180, super.key});110
@override111
Widget build(BuildContext context) {112
final scheme = Theme.of(context).colorScheme;113
return Column(114
crossAxisAlignment: CrossAxisAlignment.stretch,115
children: [116
Semantics(117
label: context.tr('chartTitle'),118
child: SizedBox(119
height: height,120
child: CustomPaint(121
painter: _RttPainter(122
series,123
grid: scheme.outlineVariant,124
label: scheme.onSurfaceVariant,125
failure: scheme.error,126
textDirection: Directionality.of(context),127
),128
),129
),130
),131
const SizedBox(height: 6),132
Wrap(133
spacing: 16,134
runSpacing: 4,135
children: [136
for (final s in series)137
Row(138
mainAxisSize: MainAxisSize.min,139
children: [140
Container(width: 12, height: 3, color: s.color),141
const SizedBox(width: 6),142
Text(s.label, style: mutedStyle(context)),143
],144
),145
Row(146
mainAxisSize: MainAxisSize.min,147
children: [148
Container(width: 3, height: 10, color: scheme.error),149
const SizedBox(width: 6),150
Text(context.tr('chartFailures'), style: mutedStyle(context)),151
],152
),153
],154
),155
],156
);157
}158
}160
class _RttPainter extends CustomPainter {161
final List<ChartSeries> series;162
final Color grid, label, failure;163
final TextDirection textDirection;164
_RttPainter(165
this.series, {166
required this.grid,167
required this.label,168
required this.failure,169
required this.textDirection,170
});172
@override173
void paint(Canvas canvas, Size size) {174
const left = 44.0, top = 10.0, bottom = 16.0, right = 6.0;175
final plot = Rect.fromLTRB(176
left,177
top,178
size.width - right,179
size.height - bottom,180
);181
if (plot.width <= 0 || plot.height <= 0) return;182
final all = [for (final s in series) ...s.samples];183
if (all.isEmpty) return;184
final start = all185
.map((s) => s.time)186
.reduce((a, b) => a.isBefore(b) ? a : b);187
final end = all.map((s) => s.time).reduce((a, b) => a.isAfter(b) ? a : b);188
final span = math.max(1, end.difference(start).inMilliseconds);189
final maxRtt = [190
for (final s in all)191
if (s.ok) s.rttMs!,192
].fold<double>(0, math.max);193
final top0 = _niceCeiling(maxRtt <= 0 ? 1 : maxRtt * 1.1);194
final gridPaint = Paint()195
..color = grid196
..strokeWidth = 1;197
for (var i = 0; i <= 3; i++) {198
final y = plot.bottom - plot.height * i / 3;199
canvas.drawLine(Offset(plot.left, y), Offset(plot.right, y), gridPaint);200
final value = top0 * i / 3;201
final text = TextPainter(202
text: TextSpan(203
text: value >= 10204
? value.toStringAsFixed(0)205
: value.toStringAsFixed(1),206
style: TextStyle(color: label, fontSize: 10),207
),208
textDirection: textDirection,209
)..layout();210
text.paint(canvas, Offset(left - text.width - 6, y - text.height / 2));211
}212
final unit = TextPainter(213
text: TextSpan(214
text: 'ms',215
style: TextStyle(color: label, fontSize: 10),216
),217
textDirection: textDirection,218
)..layout();219
unit.paint(canvas, Offset(2, plot.bottom + 2));220
double x(Sample s) =>221
plot.left +222
plot.width *223
(all.length == 1224
? 0.5225
: s.time.difference(start).inMilliseconds / span);226
double y(double rtt) => plot.bottom - plot.height * (rtt / top0);227
final failPaint = Paint()228
..color = failure229
..strokeWidth = 2;230
for (final s in series) {231
final line = Paint()232
..color = s.color233
..strokeWidth = 1.6234
..style = PaintingStyle.stroke;235
final dot = Paint()..color = s.color;236
// At most one point per horizontal pixel keeps long runs cheap.237
final step = math.max(1, (s.samples.length / plot.width).ceil());238
for (var i = 0; i < s.samples.length; i += step) {239
final sample = s.samples[i];240
if (!sample.ok) {241
final fx = x(sample);242
canvas.drawLine(243
Offset(fx, plot.top),244
Offset(fx, plot.top + 8),245
failPaint,246
);247
} else if (s.samples.length <= 120) {248
canvas.drawCircle(Offset(x(sample), y(sample.rttMs!)), 2, dot);249
}250
}251
// Draw each continuous stretch.252
_drawStretches(canvas, s, step, x, y, line);253
}254
}256
void _drawStretches(257
Canvas canvas,258
ChartSeries s,259
int step,260
double Function(Sample) x,261
double Function(double) y,262
Paint line,263
) {264
Path? path;265
void flush() {266
if (path != null) canvas.drawPath(path!, line);267
path = null;268
}270
for (var i = 0; i < s.samples.length; i += step) {271
final sample = s.samples[i];272
if (!sample.ok) {273
flush();274
continue;275
}276
final p = Offset(x(sample), y(sample.rttMs!));277
if (path == null) {278
path = Path()..moveTo(p.dx, p.dy);279
} else {280
path!.lineTo(p.dx, p.dy);281
}282
}283
flush();284
}286
static double _niceCeiling(double v) {287
final magnitude = math288
.pow(10, (math.log(v) / math.ln10).floor())289
.toDouble();290
for (final f in [1, 2, 2.5, 5, 10]) {291
if (f * magnitude >= v) return f * magnitude;292
}293
return 10 * magnitude;294
}296
@override297
bool shouldRepaint(_RttPainter old) => true;298
}300
/// The attempts of one series, one row each, in a bounded scrolling box.301
class SampleLogView extends StatefulWidget {302
final List<Sample> samples;303
final int dropped;304
const SampleLogView(this.samples, {this.dropped = 0, super.key});305
@override306
State<SampleLogView> createState() => _SampleLogViewState();307
}309
class _SampleLogViewState extends State<SampleLogView> {310
final scroll = ScrollController();311
@override312
void didUpdateWidget(SampleLogView old) {313
super.didUpdateWidget(old);314
// Follows new attempts while the user is at the end of the list.315
if (scroll.hasClients &&316
scroll.position.pixels >= scroll.position.maxScrollExtent - 40) {317
WidgetsBinding.instance.addPostFrameCallback((_) {318
if (scroll.hasClients) {319
scroll.jumpTo(scroll.position.maxScrollExtent);320
}321
});322
}323
}325
@override326
void dispose() {327
scroll.dispose();328
super.dispose();329
}331
@override332
Widget build(BuildContext context) {333
final error = Theme.of(context).colorScheme.error;334
final time = DateFormat.Hms(Localizations.localeOf(context).toString());335
final samples = widget.samples;336
return Column(337
crossAxisAlignment: CrossAxisAlignment.stretch,338
children: [339
if (widget.dropped > 0)340
Padding(341
padding: const EdgeInsets.only(bottom: 4),342
child: Text(343
'${context.tr('olderOmitted')}: ${widget.dropped}',344
style: mutedStyle(context),345
),346
),347
SizedBox(348
height: math.min(280, 30.0 * samples.length + 8),349
child: Scrollbar(350
controller: scroll,351
child: ListView.builder(352
controller: scroll,353
itemCount: samples.length,354
itemExtent: 30,355
itemBuilder: (context, i) {356
final s = samples[i];357
final ok = s.outcome == Outcome.ok;358
return Row(359
children: [360
SizedBox(361
width: 52,362
child: Text(363
'#${s.seq}',364
style: const TextStyle(fontFeatures: tabular),365
),366
),367
SizedBox(368
width: 72,369
child: Text(370
time.format(s.time.toLocal()),371
style: mutedStyle(372
context,373
).copyWith(fontFeatures: tabular),374
),375
),376
Expanded(377
child: Text(378
'${s.family.label} ${formatEndpoint(s.address, s.port)}',379
maxLines: 1,380
overflow: TextOverflow.ellipsis,381
style: mutedStyle(context),382
),383
),384
const SizedBox(width: 8),385
Text(386
[387
if (s.rttMs != null)388
'${formatMs(context, s.rttMs!)} ms',389
if (!ok) context.tr(outcomeKey(s.outcome)),390
if (!ok && s.detail != null) '(${s.detail})',391
].join(' '),392
style: TextStyle(393
color: ok ? null : error,394
fontFeatures: tabular,395
),396
),397
],398
);399
},400
),401
),402
),403
],404
);405
}406
}SHA-256
c2e6be98f362e3317d90cf3c35fa49b48039dd91ac1f426e726f0e85cbe7f4e8CH-J Proprietary Software License 1.14 · Kod sumber disediakan di bawah CH-J Proprietary Software License 1.14. Ketersediaannya tidak mengubah terma lesen atau memberikan hak tambahan.
SHA-256 arkib
46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655