CH-J SM Mobileمدیریت سرور از طریق SSH
منو
منبع منتشرشده

CH-J SM Mobile

پوشه‌ها و فایل‌های یک نسخهٔ مشخص از برنامه را مرور کنید.

دانلود ZIP منبع
CH-J / SM Mobile 175 فایل
sm-mobile-20261016T185235Z-8e93ecc1b59a افزوده‌شده: · ساخته‌شده:
net_widgets.dart افزوده‌شده: تاریخ ثبت نشده است ۱۲٫۵ KB · 406 خط
کد دانلود فایل
1import 'dart:math' as math;
2import 'package:flutter/material.dart';
3import 'package:intl/intl.dart' show DateFormat, NumberFormat;
4import '../l10n/strings.dart';
5import '../services/net/measure.dart';
6import 'adaptive.dart';
7import 'theme.dart';
9/// Series colors: IPv4 blue, IPv6 violet, failures red.
10Color familyColor(Family f) => f == Family.v4 ? Brand.blue : Brand.violet;
12TextStyle mutedStyle(BuildContext context) => TextStyle(
13 color: Theme.of(context).colorScheme.onSurfaceVariant,
14 fontSize: 12,
15);
17const tabular = [FontFeature.tabularFigures()];
19String formatMs(BuildContext context, double v) =>
20 NumberFormat.decimalPatternDigits(
21 locale: Localizations.localeOf(context).toString(),
22 decimalDigits: v < 10 ? 2 : 1,
23 ).format(v);
25String formatPercent(BuildContext context, double v) =>
26 '${NumberFormat.decimalPatternDigits(locale: Localizations.localeOf(context).toString(), decimalDigits: 1).format(v)} %';
28String 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)}';
34String 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.
44class Stat extends StatelessWidget {
45 final String label, value;
46 const Stat(this.label, this.value, {super.key});
47 @override
48 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 );
62/// The statistics of one series. RTT figures are shown only when at least
63/// one attempt succeeded.
64class StatsGrid extends StatelessWidget {
65 final RttStats stats;
66 const StatsGrid(this.stats, {super.key});
67 @override
68 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 == 0
81 ? '—'
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 }
97class ChartSeries {
98 final String label;
99 final Color color;
100 final List<Sample> samples;
101 const ChartSeries(this.label, this.color, this.samples);
104/// RTT over time for one or more series. Failed attempts are drawn as red
105/// marks along the top edge, not as invented RTT values.
106class RttChart extends StatelessWidget {
107 final List<ChartSeries> series;
108 final double height;
109 const RttChart(this.series, {this.height = 180, super.key});
110 @override
111 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 }
160class _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 @override
173 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 = all
185 .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 = grid
196 ..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 >= 10
204 ? 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 == 1
224 ? 0.5
225 : s.time.difference(start).inMilliseconds / span);
226 double y(double rtt) => plot.bottom - plot.height * (rtt / top0);
227 final failPaint = Paint()
228 ..color = failure
229 ..strokeWidth = 2;
230 for (final s in series) {
231 final line = Paint()
232 ..color = s.color
233 ..strokeWidth = 1.6
234 ..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 = math
288 .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 @override
297 bool shouldRepaint(_RttPainter old) => true;
300/// The attempts of one series, one row each, in a bounded scrolling box.
301class SampleLogView extends StatefulWidget {
302 final List<Sample> samples;
303 final int dropped;
304 const SampleLogView(this.samples, {this.dropped = 0, super.key});
305 @override
306 State<SampleLogView> createState() => _SampleLogViewState();
309class _SampleLogViewState extends State<SampleLogView> {
310 final scroll = ScrollController();
311 @override
312 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 @override
326 void dispose() {
327 scroll.dispose();
328 super.dispose();
329 }
331 @override
332 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 }
SHA-256c2e6be98f362e3317d90cf3c35fa49b48039dd91ac1f426e726f0e85cbe7f4e8

CH-J Proprietary Software License 1.14 · کد منبع تحت CH-J Proprietary Software License 1.14 ارائه می‌شود. در دسترس بودن آن شرایط مجوز را تغییر نمی‌دهد و حق اضافه‌ای اعطا نمی‌کند.

SHA-256 بایگانی46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655
فایل‌ها