Files
TelemetryMonitor/lib/ui/dialogs/settings_dialog.dart

291 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../../config/settings.dart';
import '../../transport/connection_state.dart';
import '../app_scope.dart';
class SettingsDialog extends StatefulWidget {
const SettingsDialog({super.key});
@override
State<SettingsDialog> createState() => _SettingsDialogState();
}
class _SettingsDialogState extends State<SettingsDialog> {
late Settings _draft;
late TextEditingController _wsCtrl;
late TextEditingController _pktCtrl;
late TextEditingController _logCtrl;
late TextEditingController _batchCtrl;
late TextEditingController _initCtrl;
late TextEditingController _maxCtrl;
late TextEditingController _backoffCtrl;
late TextEditingController _lookbackCtrl;
@override
void initState() {
super.initState();
final live = context.findAncestorWidgetOfExactType<AppScope>()!.settings;
_draft = live.clone();
_wsCtrl = TextEditingController(text: _draft.wsUrl);
_pktCtrl = TextEditingController(text: _draft.packetBufferCapacity.toString());
_logCtrl = TextEditingController(text: _draft.logBufferCapacity.toString());
_batchCtrl = TextEditingController(
text: _draft.decoderBatchInterval.inMilliseconds.toString());
_initCtrl = TextEditingController(
text: _draft.reconnectInitialDelay.inMilliseconds.toString());
_maxCtrl = TextEditingController(
text: _draft.reconnectMaxDelay.inMilliseconds.toString());
_backoffCtrl =
TextEditingController(text: _draft.reconnectBackoffFactor.toString());
_lookbackCtrl =
TextEditingController(text: _draft.statusLookback.toString());
}
@override
void dispose() {
for (final c in [
_wsCtrl, _pktCtrl, _logCtrl, _batchCtrl,
_initCtrl, _maxCtrl, _backoffCtrl, _lookbackCtrl,
]) {
c.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
final scope = AppScope.of(context);
return AlertDialog(
title: const Text('Settings'),
content: SizedBox(
width: 480,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_section('Connection'),
TextField(
controller: _wsCtrl,
decoration: const InputDecoration(labelText: 'WebSocket URL'),
style: const TextStyle(fontFamily: 'monospace'),
),
const SizedBox(height: 8),
ValueListenableBuilder<WsConnectionState>(
valueListenable: scope.session.connectionState,
builder: (_, state, __) {
final dotColor = switch (state) {
WsConnectionState.connected => Colors.green,
WsConnectionState.connecting ||
WsConnectionState.reconnecting => Colors.orange,
WsConnectionState.disconnected => Colors.grey,
};
final label = switch (state) {
WsConnectionState.connected => 'Connected to',
WsConnectionState.connecting => 'Connecting to',
WsConnectionState.reconnecting => 'Reconnecting to',
WsConnectionState.disconnected => 'Disconnected',
};
final url = scope.transport_url();
final urlChanged = _wsCtrl.text != url;
return Container(
padding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceContainerHighest,
borderRadius: BorderRadius.circular(6),
),
child: Row(children: [
Container(
width: 8, height: 8,
decoration: BoxDecoration(
color: dotColor, shape: BoxShape.circle),
),
const SizedBox(width: 8),
Text('$label ', style: const TextStyle(fontSize: 11)),
Text(url,
style: const TextStyle(
fontFamily: 'monospace', fontSize: 11)),
const Spacer(),
if (urlChanged)
const Text('URL changed — will reconnect on Apply',
style: TextStyle(
fontSize: 10,
fontStyle: FontStyle.italic,
color: Colors.grey)),
]),
);
},
),
_section('Buffers'),
_numberField('Packet buffer', _pktCtrl, hint: '≈ 10 min @ 1 kHz'),
_numberField('Log buffer', _logCtrl, hint: 'entries'),
_section('Decoder'),
_numberField('Batch interval', _batchCtrl, hint: 'ms (native)'),
if (kIsWeb)
const Padding(
padding: EdgeInsets.only(top: 4),
child: Text(
'Web target decodes inline; this setting is ignored.',
style: TextStyle(fontSize: 11, color: Colors.grey),
),
),
_section('Reconnect'),
_numberField('Initial delay', _initCtrl, hint: 'ms'),
_numberField('Max delay', _maxCtrl, hint: 'ms'),
_numberField('Backoff factor', _backoffCtrl, hint: '×'),
_section('Status indicators'),
_numberField('Status lookback', _lookbackCtrl,
hint: '≈ 1 s @ 1 kHz'),
_section('Dashboard mini log'),
..._severityCheckboxes(),
_section('Appearance'),
SwitchListTile(
dense: true,
contentPadding: EdgeInsets.zero,
title: const Text('Dark theme'),
value: _draft.darkMode,
onChanged: (v) => setState(() => _draft.darkMode = v),
),
],
),
),
),
actions: [
TextButton(
onPressed: () {
_draft.restoreDefaults(isWeb: kIsWeb);
setState(() {
_wsCtrl.text = _draft.wsUrl;
_pktCtrl.text = _draft.packetBufferCapacity.toString();
_logCtrl.text = _draft.logBufferCapacity.toString();
_batchCtrl.text =
_draft.decoderBatchInterval.inMilliseconds.toString();
_initCtrl.text =
_draft.reconnectInitialDelay.inMilliseconds.toString();
_maxCtrl.text =
_draft.reconnectMaxDelay.inMilliseconds.toString();
_backoffCtrl.text = _draft.reconnectBackoffFactor.toString();
_lookbackCtrl.text = _draft.statusLookback.toString();
});
},
child: const Text('Restore defaults'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => _apply(context),
child: const Text('Apply'),
),
],
);
}
Future<void> _apply(BuildContext context) async {
_draft.wsUrl = _wsCtrl.text;
_draft.packetBufferCapacity = int.tryParse(_pktCtrl.text) ??
_draft.packetBufferCapacity;
_draft.logBufferCapacity =
int.tryParse(_logCtrl.text) ?? _draft.logBufferCapacity;
_draft.decoderBatchInterval = Duration(
milliseconds: int.tryParse(_batchCtrl.text) ??
_draft.decoderBatchInterval.inMilliseconds,
);
_draft.reconnectInitialDelay = Duration(
milliseconds: int.tryParse(_initCtrl.text) ??
_draft.reconnectInitialDelay.inMilliseconds,
);
_draft.reconnectMaxDelay = Duration(
milliseconds: int.tryParse(_maxCtrl.text) ??
_draft.reconnectMaxDelay.inMilliseconds,
);
_draft.reconnectBackoffFactor =
double.tryParse(_backoffCtrl.text) ?? _draft.reconnectBackoffFactor;
_draft.statusLookback =
int.tryParse(_lookbackCtrl.text) ?? _draft.statusLookback;
// darkMode is edited directly on _draft via the switch.
final scope = AppScope.of(context);
final urlChanged = scope.settings.wsUrl != _draft.wsUrl;
scope.settings.copyFrom(_draft);
await scope.settings.save();
// Resize buffers if capacity changed.
if (scope.session.packets.capacity != _draft.packetBufferCapacity) {
scope.session.packets.resize(_draft.packetBufferCapacity);
}
if (scope.session.logs.capacity != _draft.logBufferCapacity) {
scope.session.logs.resize(_draft.logBufferCapacity);
}
if (urlChanged) {
await scope.session.reconnect();
}
if (context.mounted) Navigator.of(context).pop();
}
Widget _section(String label) => Padding(
padding: const EdgeInsets.only(top: 16, bottom: 6),
child: Text(label,
style: const TextStyle(
fontWeight: FontWeight.w500, color: Colors.grey)),
);
Widget _numberField(String label, TextEditingController c,
{String? hint}) {
return Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Row(
children: [
SizedBox(width: 130, child: Text(label)),
Expanded(
child: TextField(
controller: c,
keyboardType: TextInputType.number,
style: const TextStyle(fontFamily: 'monospace'),
),
),
if (hint != null) ...[
const SizedBox(width: 8),
SizedBox(
width: 100,
child:
Text(hint, style: const TextStyle(color: Colors.grey)),
),
],
],
),
);
}
List<Widget> _severityCheckboxes() {
const labels = {
1: 'DEBUG', 2: 'INFO', 3: 'WARN', 4: 'ERROR', 5: 'FATAL',
};
return labels.entries.map((e) {
return CheckboxListTile(
dense: true,
contentPadding: EdgeInsets.zero,
controlAffinity: ListTileControlAffinity.leading,
title: Text(e.value, style: const TextStyle(fontFamily: 'monospace')),
value: _draft.miniLogSeverities.contains(e.key),
onChanged: (v) => setState(() {
if (v == true) {
_draft.miniLogSeverities.add(e.key);
} else {
_draft.miniLogSeverities.remove(e.key);
}
}),
);
}).toList();
}
}
extension _ScopeTransport on AppScope {
// Helper: get the transport's actual current URL. Falls back to the
// settings value if transport is between connections.
String transport_url() => session.transport.currentUrl ?? settings.wsUrl;
}