feat(flutter): add optional live speed/RTT display, matching desktop GUI

Mirrors ostp-gui's "Show Speed" / "Show RTT" client settings toggles
(both default on): the home screen now shows live download/upload
throughput (computed from byte deltas between 1s polls, same as
desktop's poll()) as a subtitle under the existing cumulative
Download/Upload totals, and the RTT box is now hideable. Also fixed
"Test Ping" to actually query getMetrics instead of just faking a
500ms spinner with no real measurement.
This commit is contained in:
ospab 2026-07-10 01:18:07 +03:00
parent 1d1a1ea5af
commit 223f02287a
2 changed files with 71 additions and 4 deletions

View File

@ -32,6 +32,16 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
String _download = '0 B';
String _upload = '0 B';
// Live throughput (bytes/sec, computed from deltas between polls) and RTT
// are optional, same as the desktop GUI's "Show Speed" / "Show RTT" toggles
// in client settings default on, persisted in prefs.
bool _showSpeed = true;
bool _showRtt = true;
String _downSpeed = '0 B/s';
String _upSpeed = '0 B/s';
int _prevBytesRecv = 0;
int _prevBytesSent = 0;
late AnimationController _pulseController;
late AnimationController _spinController;
@ -73,6 +83,8 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
// happen the editor enforces exclusivity but don't crash on stale data).
final actives = profiles.where((p) => p.active).toList();
_activeProfile = actives.isNotEmpty ? actives.first : null;
_showSpeed = widget.prefs.getBool('show_speed') ?? true;
_showRtt = widget.prefs.getBool('show_rtt') ?? true;
});
_updateLatestConfigJson();
}
@ -402,6 +414,12 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
setState(() {
_download = _formatBytes(bytesRecv);
_upload = _formatBytes(bytesSent);
final dRecv = bytesRecv > _prevBytesRecv ? bytesRecv - _prevBytesRecv : 0;
final dSent = bytesSent > _prevBytesSent ? bytesSent - _prevBytesSent : 0;
_prevBytesRecv = bytesRecv;
_prevBytesSent = bytesSent;
_downSpeed = '${_formatBytes(dRecv)}/s';
_upSpeed = '${_formatBytes(dSent)}/s';
if (rttMs > 0 && !_isCheckingPing) {
_pingText = 'Server Ping: $rttMs ms';
if (rttMs < 100) {
@ -438,7 +456,30 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
_pingColor = Colors.white70;
});
await Future.delayed(const Duration(milliseconds: 500));
try {
final metricsJson = await platform.invokeMethod('getMetrics');
if (metricsJson != null && metricsJson.isNotEmpty) {
final Map<String, dynamic> parsed = jsonDecode(metricsJson);
final rttMs = parsed['rtt_ms'] as int? ?? 0;
if (mounted) {
setState(() {
if (rttMs > 0) {
_pingText = 'Server Ping: $rttMs ms';
_pingColor = rttMs < 100
? const Color(0xFF22D3A5)
: rttMs < 250
? Colors.amberAccent
: Colors.redAccent;
} else {
_pingText = 'Server Ping: -- ms';
_pingColor = Colors.white54;
}
});
}
}
} catch (e) {
debugPrint("Failed to check latency: $e");
}
if (mounted) {
setState(() {
@ -453,6 +494,10 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
_state = ConnectionStateEnum.disconnected;
_download = '0 B';
_upload = '0 B';
_downSpeed = '0 B/s';
_upSpeed = '0 B/s';
_prevBytesRecv = 0;
_prevBytesSent = 0;
_pingText = 'Target Ping: -- ms';
_pingColor = Colors.white54;
_isCheckingPing = false;
@ -725,6 +770,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
),
),
if (_showRtt)
AnimatedOpacity(
opacity: _state == ConnectionStateEnum.connected ? 1.0 : 0.0,
duration: const Duration(milliseconds: 300),
@ -809,15 +855,15 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildMetricItem(Icons.arrow_downward_rounded, 'Download', _download, theme.colorScheme.secondary),
_buildMetricItem(Icons.arrow_downward_rounded, 'Download', _download, theme.colorScheme.secondary, _showSpeed ? _downSpeed : null),
Container(width: 1, height: 40, color: Colors.white.withOpacity(0.15)),
_buildMetricItem(Icons.arrow_upward_rounded, 'Upload', _upload, theme.colorScheme.primary),
_buildMetricItem(Icons.arrow_upward_rounded, 'Upload', _upload, theme.colorScheme.primary, _showSpeed ? _upSpeed : null),
],
),
);
}
Widget _buildMetricItem(IconData icon, String label, String value, Color color) {
Widget _buildMetricItem(IconData icon, String label, String value, Color color, [String? speed]) {
return Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
@ -856,6 +902,19 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
color: Colors.white,
),
),
if (speed != null) ...[
const SizedBox(height: 2),
Text(
speed,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontFamily: 'monospace',
fontSize: 12,
fontWeight: FontWeight.w600,
color: color,
),
),
],
],
),
)

View File

@ -37,6 +37,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
bool _debugMode = false;
bool _muxEnabled = false;
bool _isCheckingUpdates = false;
bool _showSpeed = true;
bool _showRtt = true;
List<OstpProfile> _profiles = [];
@ -57,6 +59,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
// doesn't map to anything meaningful on Android the way it does on desktop.
_debugMode = widget.prefs.getBool('debug_mode') ?? false;
_muxEnabled = widget.prefs.getBool('mux_enabled') ?? false;
_showSpeed = widget.prefs.getBool('show_speed') ?? true;
_showRtt = widget.prefs.getBool('show_rtt') ?? true;
_muxSessionsCtrl = TextEditingController(text: widget.prefs.getString('mux_sessions') ?? '2');
_profiles = decodeProfiles(widget.prefs.getString('profiles_json'));
}
@ -81,6 +85,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
widget.prefs.setString('ex_ips', _ipsCtrl.text.trim());
widget.prefs.setBool('debug_mode', _debugMode);
widget.prefs.setBool('mux_enabled', _muxEnabled);
widget.prefs.setBool('show_speed', _showSpeed);
widget.prefs.setBool('show_rtt', _showRtt);
widget.prefs.setString('mux_sessions', _muxSessionsCtrl.text.trim());
widget.prefs.setString('profiles_json', encodeProfiles(_profiles));
}
@ -700,6 +706,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
_buildTextField('MUX Sessions', _muxSessionsCtrl, hint: 'e.g. 2, 4, 8'),
_buildToggle('Debug Mode', 'Verbose logging', _debugMode, (v) => _debugMode = v),
_buildToggle('Show Speed', 'Live download/upload speed on the home screen', _showSpeed, (v) => _showSpeed = v),
_buildToggle('Show RTT', 'Live server ping on the home screen', _showRtt, (v) => _showRtt = v),
_buildTextField('Local Proxy Bind', _localBindCtrl, hint: '127.0.0.1:1088'),
_buildTextField('Custom DNS Server', _dnsCtrl, hint: '1.1.1.1 (e.g. 8.8.8.8)'),