CH-J SM Mobileการจัดการเซิร์ฟเวอร์ผ่าน SSH
เมนู
ซอร์สที่เผยแพร่

CH-J SM Mobile

เรียกดูไดเรกทอรีและไฟล์ของรุ่นแอปพลิเคชันที่ระบุ

ดาวน์โหลด ZIP ของซอร์ส
CH-J / SM Mobile 175 ไฟล์
sm-mobile-20261016T185235Z-8e93ecc1b59a วันที่เพิ่ม: · วันที่บิลด์:
monitor_page.dart วันที่เพิ่ม: ไม่ได้บันทึกวันที่ 9.6 KB · 305 บรรทัด
โค้ด ดาวน์โหลดไฟล์
1import 'dart:async';
2import 'package:flutter/material.dart';
3import 'package:intl/intl.dart';
4import '../core/app_controller.dart';
5import '../l10n/strings.dart';
6import '../services/metrics_format.dart';
7import '../services/monitor.dart';
8import '../services/ssh_service.dart';
9import 'adaptive.dart';
10import 'common.dart';
11import 'theme.dart';
13class MonitorPage extends StatefulWidget {
14 final AppController controller;
15 const MonitorPage(this.controller, {super.key});
16 @override
17 State<MonitorPage> createState() => _MonitorPageState();
20class _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 @override
28 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 @override
71 void dispose() {
72 timer?.cancel();
73 adapter.cancel();
74 widget.controller.removeListener(schedule);
75 super.dispose();
76 }
78 @override
79 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 running
115 // is ignored by refresh() instead of starting a second one.
116 FilledButton.tonalIcon(
117 onPressed: refresh,
118 icon: busy
119 ? 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 == null
160 ? metrics['ram']
161 : percent(v.ramPercent!),
162 detail: v.ramUsedBytes == null
163 ? null
164 : 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 == null
172 ? metrics['disk']
173 : percent(v.diskPercent!),
174 detail: v.diskUsedBytes == null
175 ? null
176 : 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 == null
187 ? 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 }
225TextStyle _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 and
231/// an optional percentage gauge.
232class _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 @override
247 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 >= .9
286 ? scheme.error
287 : level >= .75
288 ? Brand.amber
289 : 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 }
SHA-2569b644075b91c40aef757de2700c656cfe2fc1b8117456d617dfd2eec4b9b7e76

CH-J Proprietary Software License 1.14 · ซอร์สโค้ดนี้ให้ไว้ภายใต้ CH-J Proprietary Software License 1.14 การเปิดให้เข้าถึงไม่ได้เปลี่ยนแปลงเงื่อนไขของสัญญาอนุญาตหรือให้สิทธิ์เพิ่มเติม

SHA-256 ของไฟล์บีบอัด46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655
ไฟล์