Veröffentlichte Quellen
Quellen als ZIP herunterladen
CH-J SM Mobile
Durchsuchen Sie Verzeichnisse und Dateien einer bestimmten Anwendungsausgabe.
CH-J / SM Mobile
175 Dateien
sm-mobile-20261016T185235Z-8e93ecc1b59a
Hinzugefügt: · Erstellt:
1
import 'dart:async';2
import 'package:flutter/material.dart';3
import 'package:intl/intl.dart';4
import '../core/app_controller.dart';5
import '../l10n/strings.dart';6
import '../services/metrics_format.dart';7
import '../services/monitor.dart';8
import '../services/ssh_service.dart';9
import 'adaptive.dart';10
import 'common.dart';11
import 'theme.dart';13
class MonitorPage extends StatefulWidget {14
final AppController controller;15
const MonitorPage(this.controller, {super.key});16
@override17
State<MonitorPage> createState() => _MonitorPageState();18
}20
class _MonitorPageState extends State<MonitorPage> {21
final adapter = LinuxMonitor();22
Map<String, String> metrics = {};23
Timer? timer;24
bool busy = false;25
String? error;26
DateTime? updated;27
@override28
void initState() {29
super.initState();30
widget.controller.addListener(schedule);31
schedule();32
}34
void schedule() {35
timer?.cancel();36
if (!widget.controller.foreground || !widget.controller.ssh.connected) {37
adapter.cancel();38
return;39
}40
refresh();41
timer = Timer.periodic(42
Duration(seconds: widget.controller.monitorSeconds),43
(_) => refresh(),44
);45
}47
Future<void> refresh() async {48
if (busy ||49
!widget.controller.foreground ||50
!widget.controller.ssh.connected) {51
return;52
}53
setState(() => busy = true);54
try {55
final result = await adapter.sample(widget.controller.ssh.client);56
if (mounted && widget.controller.foreground) {57
setState(() {58
metrics = result;59
error = null;60
updated = DateTime.now();61
});62
}63
} catch (e) {64
if (mounted) setState(() => error = failureCode(e));65
} finally {66
if (mounted) setState(() => busy = false);67
}68
}70
@override71
void dispose() {72
timer?.cancel();73
adapter.cancel();74
widget.controller.removeListener(schedule);75
super.dispose();76
}78
@override79
Widget build(BuildContext context) {80
if (!widget.controller.ssh.connected) {81
return const PageMessage(82
'selectServer',83
icon: Icons.monitor_heart_outlined,84
);85
}86
final locale = Localizations.localeOf(context).toString();87
final l = AppLocalizations.of(context)!;88
String decimal(double v, int digits) => NumberFormat.decimalPatternDigits(89
locale: locale,90
decimalDigits: digits,91
).format(v);92
String percent(double v) => '${decimal(v, 1)} %';93
String bytes(int b) => formatBytes(b, decimal);94
final v = MetricValues.parse(metrics);95
final handshake = widget.controller.ssh.handshakeMs;96
String uptime() {97
final u = splitUptime(v.uptimeSeconds!);98
return [99
if (u.days > 0) l.uptimeDays(u.days),100
if (u.days > 0 || u.hours > 0) l.uptimeHours(u.hours),101
l.uptimeMinutes(u.minutes),102
].join(' ');103
}105
return LayoutBuilder(106
builder: (context, constraints) => ListView(107
padding: readablePadding(constraints, maxWidth: 1200),108
children: [109
Wrap(110
spacing: 12,111
runSpacing: 8,112
crossAxisAlignment: WrapCrossAlignment.center,113
children: [114
// Never shown as disabled: a press while a sample is running115
// is ignored by refresh() instead of starting a second one.116
FilledButton.tonalIcon(117
onPressed: refresh,118
icon: busy119
? const SizedBox.square(120
dimension: 18,121
child: CircularProgressIndicator(strokeWidth: 2),122
)123
: const Icon(Icons.refresh),124
label: Text(context.tr('refresh')),125
),126
Text(127
[128
l.autoRefresh('${widget.controller.monitorSeconds}'),129
if (updated != null)130
l.updatedAt(DateFormat.Hms(locale).format(updated!)),131
].join(' · '),132
style: _muted(context),133
),134
],135
),136
const SizedBox(height: 12),137
if (error != null)138
Card(139
color: Theme.of(context).colorScheme.errorContainer,140
child: ListTile(141
leading: const Icon(Icons.error_outline),142
title: Text(context.tr(error!)),143
),144
),145
AdaptiveGrid(146
minTileWidth: 280,147
children: [148
_Metric(149
icon: Icons.memory,150
color: Brand.blue,151
label: context.tr('cpu'),152
value: v.cpu == null ? metrics['cpu'] : percent(v.cpu!),153
gauge: v.cpu,154
),155
_Metric(156
icon: Icons.developer_board,157
color: Brand.violet,158
label: context.tr('ram'),159
value: v.ramPercent == null160
? metrics['ram']161
: percent(v.ramPercent!),162
detail: v.ramUsedBytes == null163
? null164
: l.usedOf(bytes(v.ramUsedBytes!), bytes(v.ramTotalBytes!)),165
gauge: v.ramPercent,166
),167
_Metric(168
icon: Icons.storage,169
color: Brand.green,170
label: context.tr('disk'),171
value: v.diskPercent == null172
? metrics['disk']173
: percent(v.diskPercent!),174
detail: v.diskUsedBytes == null175
? null176
: l.usedOf(177
bytes(v.diskUsedBytes!),178
bytes(v.diskTotalBytes!),179
),180
gauge: v.diskPercent,181
),182
_Metric(183
icon: Icons.av_timer,184
color: Brand.blue,185
label: context.tr('load'),186
value: v.load == null187
? metrics['load']188
: v.load!.map((x) => decimal(x, 2)).join(' · '),189
detail: v.load == null ? null : context.tr('loadNote'),190
),191
_Metric(192
icon: Icons.schedule,193
color: Brand.green,194
label: context.tr('uptime'),195
value: v.uptimeSeconds == null ? metrics['uptime'] : uptime(),196
),197
_Metric(198
icon: Icons.dns_outlined,199
color: Brand.violet,200
label: context.tr('release'),201
value: metrics['release'],202
detail: metrics['system'],203
),204
_Metric(205
icon: Icons.speed,206
color: Brand.blue,207
label: context.tr('sshLatency'),208
value: metrics['rtt'],209
detail: [210
context.tr('commandRtt'),211
if (handshake != null)212
'${context.tr('handshake')}: $handshake ms',213
].join('\n'),214
),215
],216
),217
const SizedBox(height: 12),218
Text(context.tr('backgroundNote'), style: _muted(context)),219
],220
),221
);222
}223
}225
TextStyle _muted(BuildContext context) => TextStyle(226
color: Theme.of(context).colorScheme.onSurfaceVariant,227
fontSize: 12,228
);230
/// A desktop-style metric card: tinted icon tile, label, value, detail and231
/// an optional percentage gauge.232
class _Metric extends StatelessWidget {233
final IconData icon;234
final Color color;235
final String label;236
final String? value, detail;237
final double? gauge;238
const _Metric({239
required this.icon,240
required this.color,241
required this.label,242
required this.value,243
this.detail,244
this.gauge,245
});246
@override247
Widget build(BuildContext context) {248
final scheme = Theme.of(context).colorScheme;249
final level = gauge == null ? null : (gauge! / 100).clamp(0.0, 1.0);250
return Card(251
child: Padding(252
padding: const EdgeInsets.all(16),253
child: Row(254
crossAxisAlignment: CrossAxisAlignment.start,255
children: [256
Container(257
width: 42,258
height: 42,259
decoration: BoxDecoration(260
color: color.withValues(alpha: .12),261
borderRadius: BorderRadius.circular(12),262
),263
child: Icon(icon, color: color),264
),265
const SizedBox(width: 14),266
Expanded(267
child: Column(268
crossAxisAlignment: CrossAxisAlignment.start,269
children: [270
Text(label, style: _muted(context)),271
const SizedBox(height: 4),272
SelectableText(273
value ?? '—',274
style: Theme.of(context).textTheme.titleLarge?.copyWith(275
fontFeatures: const [FontFeature.tabularFigures()],276
),277
),278
if (level != null) ...[279
const SizedBox(height: 8),280
ClipRRect(281
borderRadius: BorderRadius.circular(3),282
child: LinearProgressIndicator(283
value: level,284
minHeight: 5,285
color: level >= .9286
? scheme.error287
: level >= .75288
? Brand.amber289
: color,290
),291
),292
],293
if (detail != null) ...[294
const SizedBox(height: 6),295
Text(detail!, style: _muted(context)),296
],297
],298
),299
),300
],301
),302
),303
);304
}305
}SHA-256
9b644075b91c40aef757de2700c656cfe2fc1b8117456d617dfd2eec4b9b7e76CH-J Proprietary Software License 1.14 · Die Quellen werden unter der CH-J Proprietary Software License 1.14 bereitgestellt. Ihre Verfügbarkeit ändert die Lizenzbedingungen nicht und gewährt keine zusätzlichen Rechte.
SHA-256 des Archivs
46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655