Files
flutter_server_box/lib/view/page/process.dart
GT610 d14e97485f refactor: Optimize process page (#1017)
* refactor(process): Optimize the sorting logic of the process list and add a data integrity check flag

Add the `_checkedIncompleteData` flag to avoid repeatedly checking data integrity

Change `_sortModes` to a final variable and initialize it using `List.from`

Remove unnecessary calls to `_timer.cancel`

* refactor(process): Remove unused focus states and refactor process item operations

Move the stop operation of the process item to the tail button, and remove the focus state-related code that is no longer used

* fix(process): Fix the layout of the process page and the stop confirmation dialog box

Adjust the spacing condition between CPU and memory display, and only add spacing when both are present

Modify the confirmation dialog for stopping the process, and add a cancel button to provide a better user experience

* refactor(process): Merge duplicate sorting mode removal logic

Combine two separate `removeWhere` calls into one, simplifying the code and enhancing readability
2026-01-22 00:22:23 +08:00

183 lines
5.7 KiB
Dart

import 'dart:async';
import 'package:dartssh2/dartssh2.dart';
import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/core/route.dart';
import 'package:server_box/data/model/app/scripts/shell_func.dart';
import 'package:server_box/data/model/server/proc.dart';
import 'package:server_box/data/provider/server/single.dart';
import 'package:server_box/data/res/store.dart';
class ProcessPage extends ConsumerStatefulWidget {
final SpiRequiredArgs args;
const ProcessPage({super.key, required this.args});
@override
ConsumerState<ProcessPage> createState() => _ProcessPageState();
static const route = AppRouteArg(page: ProcessPage.new, path: '/process');
}
class _ProcessPageState extends ConsumerState<ProcessPage> {
late Timer _timer;
late MediaQueryData _media;
SSHClient? _client;
PsResult _result = const PsResult(procs: []);
bool _checkedIncompleteData = false;
// Issue #64
// In cpu mode, the process list will change in a high frequency.
// So user will easily know that the list is refreshed.
ProcSortMode _procSortMode = ProcSortMode.cpu;
final _sortModes = List<ProcSortMode>.from(ProcSortMode.values);
late final _provider = serverProvider(widget.args.spi.id);
@override
void dispose() {
super.dispose();
_timer.cancel();
}
@override
void initState() {
super.initState();
final serverState = ref.read(_provider);
_client = serverState.client;
final duration = Duration(seconds: Stores.setting.serverStatusUpdateInterval.fetch());
_timer = Timer.periodic(duration, (_) => _refresh());
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_media = MediaQuery.of(context);
}
Future<void> _refresh() async {
if (mounted) {
final serverState = ref.read(_provider);
final systemType = serverState.status.system;
final result = await _client
?.run(ShellFunc.process.exec(widget.args.spi.id, systemType: systemType, customDir: null))
.string;
if (result == null || result.isEmpty) {
context.showSnackBar(libL10n.empty);
return;
}
_result = PsResult.parse(result, sort: _procSortMode);
if (!_checkedIncompleteData) {
final isAnyProcDataNotComplete = _result.procs.any((e) => e.cpu == null || e.mem == null);
if (isAnyProcDataNotComplete) {
_sortModes.removeWhere((e) => e == ProcSortMode.cpu || e == ProcSortMode.mem);
}
_checkedIncompleteData = true;
}
setState(() {});
}
}
@override
Widget build(BuildContext context) {
final actions = <Widget>[
PopupMenuButton<ProcSortMode>(
onSelected: (value) {
setState(() {
_procSortMode = value;
});
},
icon: const Icon(Icons.sort),
initialValue: _procSortMode,
itemBuilder: (_) => _sortModes.map((e) => PopupMenuItem(value: e, child: Text(e.name))).toList(),
),
];
if (_result.error != null) {
actions.add(
IconButton(
icon: const Icon(Icons.error),
onPressed: () => context.showRoundDialog(
title: libL10n.error,
child: SingleChildScrollView(child: Text(_result.error!)),
actions: [TextButton(onPressed: () => Pfs.copy(_result.error!), child: Text(libL10n.copy))],
),
),
);
}
Widget child;
if (_result.procs.isEmpty) {
child = UIs.centerLoading;
} else {
child = ListView.builder(
itemCount: _result.procs.length,
padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 7),
itemBuilder: (_, idx) => _buildListItem(_result.procs[idx]),
);
}
return Scaffold(
appBar: CustomAppBar(
centerTitle: true,
title: TwoLineText(up: widget.args.spi.name, down: l10n.process),
actions: actions,
),
body: child,
);
}
Widget _buildListItem(Proc proc) {
final leading = proc.user == null
? Text(proc.pid.toString())
: TwoLineText(up: proc.pid.toString(), down: proc.user!);
return CardX(
key: ValueKey(proc.pid),
child: ListTile(
leading: SizedBox(width: _media.size.width / 6, child: leading),
title: Text(proc.binary),
subtitle: Text(proc.command, style: UIs.textGrey, maxLines: 3, overflow: TextOverflow.fade),
trailing: _buildItemTrail(proc),
),
);
}
Widget _buildItemTrail(Proc proc) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (proc.cpu != null) TwoLineText(up: proc.cpu!.toStringAsFixed(1), down: 'cpu'),
if (proc.cpu != null && proc.mem != null) UIs.width13,
if (proc.mem != null) TwoLineText(up: proc.mem!.toStringAsFixed(1), down: 'mem'),
if (proc.cpu != null || proc.mem != null) UIs.width13,
IconButton(
icon: const Icon(Icons.stop),
onPressed: () {
context.showRoundDialog(
title: libL10n.attention,
child: Text(libL10n.askContinue('${l10n.stop} ${l10n.process}(${proc.pid})')),
actions: [
Btn.cancel(),
Btn.ok(
onTap: () async {
context.pop();
await context.showLoadingDialog(
fn: () async {
await _client?.run('kill ${proc.pid}');
await _refresh();
},
);
},
),
],
);
},
),
],
);
}
}