diff --git a/lib/core/route.dart b/lib/core/route.dart index c727e7ad..6ec97f68 100644 --- a/lib/core/route.dart +++ b/lib/core/route.dart @@ -10,7 +10,7 @@ import 'package:toolbox/view/page/ping.dart'; import 'package:toolbox/view/page/private_key/edit.dart'; import 'package:toolbox/view/page/private_key/list.dart'; import 'package:toolbox/view/page/pve.dart'; -import 'package:toolbox/view/page/server/detail.dart'; +import 'package:toolbox/view/page/server/detail/view.dart'; import 'package:toolbox/view/page/setting/platform/android.dart'; import 'package:toolbox/view/page/setting/platform/ios.dart'; import 'package:toolbox/view/page/setting/seq/srv_func_seq.dart'; diff --git a/lib/data/model/app/range.dart b/lib/data/model/app/range.dart new file mode 100644 index 00000000..c11d0f27 --- /dev/null +++ b/lib/data/model/app/range.dart @@ -0,0 +1,11 @@ +final class Range { + final T start; + final T end; + + Range(this.start, this.end); + + bool contains(int value) => value >= start && value <= end; + + @override + String toString() => 'Range($start, $end)'; +} \ No newline at end of file diff --git a/lib/data/model/server/cpu.dart b/lib/data/model/server/cpu.dart index 201bdde8..fca0766b 100644 --- a/lib/data/model/server/cpu.dart +++ b/lib/data/model/server/cpu.dart @@ -1,9 +1,13 @@ import 'dart:collection'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:toolbox/data/model/app/range.dart'; import 'package:toolbox/data/model/server/time_seq.dart'; import 'package:toolbox/data/res/status.dart'; -class Cpus extends TimeSeq> { +const _kCap = 30; + +class Cpus extends TimeSeq> { Cpus(super.init1, super.init2); @override @@ -14,6 +18,8 @@ class Cpus extends TimeSeq> { _sys = _getSys(); _iowait = _getIowait(); _idle = _getIdle(); + _updateSpots(); + _updateRange(); } double usedPercent({int coreIdx = 0}) { @@ -60,9 +66,53 @@ class Cpus extends TimeSeq> { double _idle = 0; double get idle => _idle; double _getIdle() => 100 - usedPercent(); + + /// [core1, core2] + /// core1: [FlSpot(0, 10), FlSpot(1, 20), FlSpot(2, 30)] + final _spots = >[]; + List> get spots => _spots; + void _updateSpots() { + for (var i = 1; i < now.length; i++) { + if (i >= _spots.length) { + _spots.add(Fifo(capacity: _kCap)); + } else { + final item = _spots[i]; + final spot = FlSpot(item.count.toDouble(), usedPercent(coreIdx: i)); + item.add(spot); + } + } + } + + var _rangeX = Range(0.0, _kCap.toDouble()); + Range get rangeX => _rangeX; + // var _rangeY = Range(0.0, 100.0); + // Range get rangeY => _rangeY; + void _updateRange() { + double? minX, maxX; + for (var i = 1; i < now.length; i++) { + final item = _spots[i]; + if (item.isEmpty) continue; + final first = item.first.x; + final last = item.last.x; + if (minX == null || first < minX) minX = first; + if (maxX == null || last > maxX) maxX = last; + } + if (minX != null && maxX != null) _rangeX = Range(minX, maxX); + + // double? minY, maxY; + // for (var i = 1; i < now.length; i++) { + // final item = _spots[i]; + // if (item.isEmpty) continue; + // final first = item.first.y; + // final last = item.last.y; + // if (minY == null || first < minY) minY = first; + // if (maxY == null || last > maxY) maxY = last; + // } + // if (minY != null && maxY != null) _rangeY = Range(minY, maxY); + } } -class OneTimeCpuStatus extends TimeSeqIface { +class SingleCoreCpu extends TimeSeqIface { final String id; final int user; final int sys; @@ -72,7 +122,7 @@ class OneTimeCpuStatus extends TimeSeqIface { final int irq; final int softirq; - OneTimeCpuStatus( + SingleCoreCpu( this.id, this.user, this.sys, @@ -86,10 +136,10 @@ class OneTimeCpuStatus extends TimeSeqIface { int get total => user + sys + nice + idle + iowait + irq + softirq; @override - bool same(OneTimeCpuStatus other) => id == other.id; + bool same(SingleCoreCpu other) => id == other.id; - static List parse(String raw) { - final List cpus = []; + static List parse(String raw) { + final List cpus = []; for (var item in raw.split('\n')) { if (item == '') break; @@ -97,7 +147,7 @@ class OneTimeCpuStatus extends TimeSeqIface { if (id == null) continue; final matches = item.replaceFirst(id, '').trim().split(' '); cpus.add( - OneTimeCpuStatus( + SingleCoreCpu( id, int.parse(matches[0]), int.parse(matches[1]), @@ -128,7 +178,7 @@ Cpus parseBsdCpu(String raw) { final init = InitStatus.cpus; init.add([ - OneTimeCpuStatus('cpu', percents[0].toInt(), 0, 0, + SingleCoreCpu('cpu', percents[0].toInt(), 0, 0, percents[2].toInt() + percents[1].toInt(), 0, 0, 0), ]); return init; diff --git a/lib/data/model/server/server_status_update_req.dart b/lib/data/model/server/server_status_update_req.dart index a2611d0c..9bb3a1e4 100644 --- a/lib/data/model/server/server_status_update_req.dart +++ b/lib/data/model/server/server_status_update_req.dart @@ -69,7 +69,7 @@ Future _getLinuxStatus(ServerStatusUpdateReq req) async { } try { - final cpus = OneTimeCpuStatus.parse(StatusCmdType.cpu.find(segments)); + final cpus = SingleCoreCpu.parse(StatusCmdType.cpu.find(segments)); req.ss.cpu.update(cpus); } catch (e, s) { Loggers.parse.warning(e, s); diff --git a/lib/data/model/server/time_seq.dart b/lib/data/model/server/time_seq.dart index f1106b98..fc930b86 100644 --- a/lib/data/model/server/time_seq.dart +++ b/lib/data/model/server/time_seq.dart @@ -1,21 +1,16 @@ import 'dart:collection'; -/// A FIFO queue with fixed capacity. -abstract class TimeSeq> extends ListBase { +class Fifo extends ListBase { final int capacity; late final List _list; + var _count = 0; - /// Due to the design, at least two elements are required, otherwise [pre] / - /// [now] will throw. - TimeSeq( - T init1, - T init2, { - this.capacity = 30, - }) : _list = [init1, init2]; + Fifo({this.capacity = 30, List? list}) : _list = list ?? []; @override void add(element) { - if (length == capacity) { + _count++; + if (_list.length == capacity) { _list.removeAt(0); } _list.add(element); @@ -24,6 +19,8 @@ abstract class TimeSeq> extends ListBase { @override int get length => _list.length; + int get count => _count; + @override set length(int newLength) { throw UnimplementedError(); @@ -38,6 +35,16 @@ abstract class TimeSeq> extends ListBase { void operator []=(int index, value) { _list[index] = value; } +} + +abstract class TimeSeq> extends Fifo { + /// Due to the design, at least two elements are required, otherwise [pre] / + /// [now] will throw. + TimeSeq( + T init1, + T init2, { + super.capacity, + }) : super(list: [init1, init2]); T get pre { return _list[length - 2]; diff --git a/lib/data/res/status.dart b/lib/data/res/status.dart index 7f2aa5c9..596207db 100644 --- a/lib/data/res/status.dart +++ b/lib/data/res/status.dart @@ -9,7 +9,7 @@ import '../model/server/conn.dart'; import '../model/server/system.dart'; abstract final class InitStatus { - static OneTimeCpuStatus get _initOneTimeCpuStatus => OneTimeCpuStatus( + static SingleCoreCpu get _initOneTimeCpuStatus => SingleCoreCpu( 'cpu', 0, 0, diff --git a/lib/view/page/server/detail/misc.dart b/lib/view/page/server/detail/misc.dart new file mode 100644 index 00000000..aee09a38 --- /dev/null +++ b/lib/view/page/server/detail/misc.dart @@ -0,0 +1,113 @@ +part of 'view.dart'; + +enum _NetSortType { + device, + trans, + recv, + ; + + bool get isDevice => this == _NetSortType.device; + bool get isIn => this == _NetSortType.recv; + bool get isOut => this == _NetSortType.trans; + + _NetSortType get next { + switch (this) { + case device: + return trans; + case _NetSortType.trans: + return recv; + case recv: + return device; + } + } + + int Function(String, String) getSortFunc(NetSpeed ns) { + switch (this) { + case _NetSortType.device: + return (b, a) => a.compareTo(b); + case _NetSortType.recv: + return (b, a) => ns + .speedInBytes(ns.deviceIdx(a)) + .compareTo(ns.speedInBytes(ns.deviceIdx(b))); + case _NetSortType.trans: + return (b, a) => ns + .speedOutBytes(ns.deviceIdx(a)) + .compareTo(ns.speedOutBytes(ns.deviceIdx(b))); + } + } +} + +Widget _buildLineChart(List> spots, Range x) { + return LineChart(LineChartData( + lineTouchData: LineTouchData( + touchTooltipData: LineTouchTooltipData( + tooltipPadding: const EdgeInsets.all(8), + tooltipRoundedRadius: 8, + getTooltipItems: (List touchedSpots) { + return touchedSpots.map((e) { + return LineTooltipItem( + 'CPU${e.barIndex}: ${e.y.toStringAsFixed(2)}', + const TextStyle( + fontWeight: FontWeight.bold, + ), + ); + }).toList(); + }, + ), + handleBuiltInTouches: true, + ), + gridData: FlGridData( + show: true, + drawVerticalLine: false, + horizontalInterval: 20, + getDrawingHorizontalLine: (value) { + return const FlLine( + color: Color(0xff37434d), + strokeWidth: 1, + ); + }, + ), + titlesData: FlTitlesData( + show: true, + rightTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + topTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + bottomTitles: const AxisTitles( + sideTitles: SideTitles(showTitles: false), + ), + leftTitles: AxisTitles( + sideTitles: SideTitles( + showTitles: true, + interval: 1, + getTitlesWidget: (val, meta) { + if (val % 20 != 0) return UIs.placeholder; + return Text( + val.toInt().toString(), + style: UIs.text12Grey, + ); + }, + reservedSize: 42, + ), + ), + ), + borderData: FlBorderData(show: false), + minX: x.start, + maxX: x.end, + minY: 0, + maxY: 100, + lineBarsData: spots + .map((e) => LineChartBarData( + spots: e, + isCurved: false, + barWidth: 2, + isStrokeCapRound: true, + color: primaryColor, + dotData: const FlDotData(show: false), + belowBarData: BarAreaData(show: false), + )) + .toList(), + )); +} diff --git a/lib/view/page/server/detail.dart b/lib/view/page/server/detail/view.dart similarity index 92% rename from lib/view/page/server/detail.dart rename to lib/view/page/server/detail/view.dart index 7afa2fe0..c2b21b2d 100644 --- a/lib/view/page/server/detail.dart +++ b/lib/view/page/server/detail/view.dart @@ -1,13 +1,14 @@ +import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:icons_plus/icons_plus.dart'; import 'package:provider/provider.dart'; import 'package:toolbox/core/extension/context/common.dart'; import 'package:toolbox/core/extension/context/dialog.dart'; import 'package:toolbox/core/extension/context/locale.dart'; +import 'package:toolbox/data/model/app/range.dart'; import 'package:toolbox/data/model/app/server_detail_card.dart'; import 'package:toolbox/data/model/app/shell_func.dart'; import 'package:toolbox/data/model/server/battery.dart'; -import 'package:toolbox/data/model/server/cpu.dart'; import 'package:toolbox/data/model/server/disk.dart'; import 'package:toolbox/data/model/server/net_speed.dart'; import 'package:toolbox/data/model/server/nvdia.dart'; @@ -19,14 +20,16 @@ import 'package:toolbox/view/widget/expand_tile.dart'; import 'package:toolbox/view/widget/kv_row.dart'; import 'package:toolbox/view/widget/server_func_btns.dart'; -import '../../../core/extension/numx.dart'; -import '../../../core/route.dart'; -import '../../../data/model/server/server.dart'; -import '../../../data/provider/server.dart'; -import '../../../data/res/color.dart'; -import '../../../data/res/ui.dart'; -import '../../widget/appbar.dart'; -import '../../widget/cardx.dart'; +import '../../../../core/extension/numx.dart'; +import '../../../../core/route.dart'; +import '../../../../data/model/server/server.dart'; +import '../../../../data/provider/server.dart'; +import '../../../../data/res/color.dart'; +import '../../../../data/res/ui.dart'; +import '../../../widget/appbar.dart'; +import '../../../widget/cardx.dart'; + +part 'misc.dart'; class ServerDetailPage extends StatefulWidget { const ServerDetailPage({super.key, required this.spi}); @@ -183,12 +186,21 @@ class _ServerDetailPageState extends State ), ), childrenPadding: const EdgeInsets.symmetric(vertical: 13), - initiallyExpanded: _getInitExpand(ss.cpu.coresCount), + initiallyExpanded: true, trailing: Row( mainAxisSize: MainAxisSize.min, children: details, ), - children: _buildCPUProgress(ss.cpu), + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 7), + child: SizedBox( + height: 137, + width: _media.size.width - 26 - 34, + child: _buildLineChart(ss.cpu.spots, ss.cpu.rangeX), + ), + ), + ], ), ); } @@ -213,20 +225,6 @@ class _ServerDetailPageState extends State ); } - List _buildCPUProgress(Cpus cs) { - final children = []; - for (var i = 0; i < cs.coresCount; i++) { - if (i == 0) continue; - children.add( - Padding( - padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 17), - child: _buildProgress(cs.usedPercent(coreIdx: i)), - ), - ); - } - return children; - } - Widget _buildProgress(double percent) { if (percent > 100) percent = 100; final percentWithinOne = percent / 100; @@ -756,40 +754,3 @@ class _ServerDetailPageState extends State return len <= (max ?? 3); } } - -enum _NetSortType { - device, - trans, - recv, - ; - - bool get isDevice => this == _NetSortType.device; - bool get isIn => this == _NetSortType.recv; - bool get isOut => this == _NetSortType.trans; - - _NetSortType get next { - switch (this) { - case device: - return trans; - case _NetSortType.trans: - return recv; - case recv: - return device; - } - } - - int Function(String, String) getSortFunc(NetSpeed ns) { - switch (this) { - case _NetSortType.device: - return (b, a) => a.compareTo(b); - case _NetSortType.recv: - return (b, a) => ns - .speedInBytes(ns.deviceIdx(a)) - .compareTo(ns.speedInBytes(ns.deviceIdx(b))); - case _NetSortType.trans: - return (b, a) => ns - .speedOutBytes(ns.deviceIdx(a)) - .compareTo(ns.speedOutBytes(ns.deviceIdx(b))); - } - } -} diff --git a/pubspec.lock b/pubspec.lock index 4e669187..85ed82e1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -339,6 +339,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + fl_chart: + dependency: "direct main" + description: + name: fl_chart + sha256: "2b7c1f5d867da9a054661641c8f499c55c47c39acccb97b3bc673f5fa9a39e74" + url: "https://pub.dev" + source: hosted + version: "0.67.0" flutter: dependency: "direct main" description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index dff5d04e..5a3ecf02 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -74,6 +74,7 @@ dependencies: flutter_background_service: ^5.0.5 icons_plus: ^5.0.0 permission_handler: ^11.3.1 + fl_chart: ^0.67.0 dev_dependencies: flutter_native_splash: ^2.1.6